我使用預設的 System.Text.Json 反序列化時 JsonSerializer.Deserialize<Dictionary<string, object>>(json),得到 JsonElement,再透過 JsonElement.Get 系列的方法才能取得正確的資料,這樣有點繁瑣,為此我找到了解方,自行實作 JsonConverter,緊接著,來看看我怎麼處理的
前言
.NET 6 替 System.Text.Json 新增 DOM 的控制物件:JsonNode、JsonArray、JsonObject、JsonValue 這可以更有效率的處理 Json 結構,如需詳細資訊,請參閱 JSON DOM 選項。
有兩種方式建立 Json DOM,JsonDocument、JsonNode,在和 JsonNode 之間 JsonDocument 選擇時,請考慮下列因素:
- JsonNode:可以在建立之後變更。
- JsonDocument:唯讀,可讓您更快速地存取其資料。
開發環境
- .NET 6
- Windows 11
問題描述
反序列化後無法取得正確的型別,代碼如下:
[TestMethod]
public void 字串轉Dictionary_失敗()
{
var expected = new Dictionary<string, object>
{
["i"] = 255,
["s"] = "字串",
["d"] = new DateTime(1900, 1, 1),
["a"] = new[] { 1, 2 },
["o"] = new { Prop = 123 }
};
var json = JsonSerializer.Serialize(expected);
var actual = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
Assert.AreNotEqual(expected["i"], actual["i"]);
Assert.AreNotEqual(expected["s"], actual["s"]);
// 反序列化之後得到 JsonElement Type,必須要要透過其他手段才能取得真實的值
Assert.AreEqual("JsonElement", actual["s"].GetType().Name);
Assert.AreEqual(expected["i"], ((JsonElement)actual["i"]).GetInt32());
Assert.AreEqual(expected["s"], ((JsonElement)actual["s"]).GetString());
}
可以看出反序列化後,會得到 JsonElement ,我希望能直接取得正確的資料,不用手動轉換
實作 JsonConverter<Dictionary<string, object>>
我參考了下篇的做法,並把它捏成我想要的樣子
Custom Dictionary JsonConverter using System.Text.Json (josef.codes)
反序列化時,會使用 Utf8JsonReader 把資料讀進來,根據 JsonTokenType 調用正確的 Utf8JsonReader.Get 系列的方法,這裡要複寫 Read 方法
public class DictionaryStringObjectJsonConverter : JsonConverter<Dictionary<string, object>>
{
public override Dictionary<string, object> Read(ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException($"JsonTokenType was of type {reader.TokenType}, only objects are supported");
}
var results = new Dictionary<string, object>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return results;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("JsonTokenType was not PropertyName");
}
var propertyName = reader.GetString();
if (string.IsNullOrWhiteSpace(propertyName))
{
throw new JsonException("Failed to get property name");
}
reader.Read();
results.Add(propertyName, this.ReadValue(ref reader, options));
}
return results;
}
private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.String:
if (reader.TryGetDateTimeOffset(out var dateOffset))
{
return dateOffset;
}
if (reader.TryGetGuid(out var guid))
{
return guid;
}
return reader.GetString();
case JsonTokenType.False:
case JsonTokenType.True:
return reader.GetBoolean();
case JsonTokenType.Null:
return null;
case JsonTokenType.Number:
if (reader.TryGetInt64(out var result))
{
return result;
}
return reader.GetDecimal();
case JsonTokenType.StartObject:
return this.Read(ref reader, null, options);
case JsonTokenType.StartArray:
var list = new List<object>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
list.Add(this.ReadValue(ref reader, options));
}
return list;
default:
throw new JsonException($"'{reader.TokenType}' is not supported");
}
}
}
ReadValue 的工作就是把 Json 物件(JsonTokenType.StartObject) 轉成匿名型別
序列化時,會使用 Utf8JsonWriter,把資料寫成 Json 文字,預設的序列化沒有問題,可以不用處理,不過為了怕誤用 JsonConverter 而衍生錯誤,我還是把它實作出來。
public class DictionaryStringObjectJsonConverter : JsonConverter<Dictionary<string, object>>
{
public override void Write(Utf8JsonWriter writer,
Dictionary<string, object> value,
JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var key in value.Keys)
{
WriteValue(writer, key, value[key], options);
}
writer.WriteEndObject();
}
private static void WriteValue(Utf8JsonWriter writer,
string key,
object value,
JsonSerializerOptions options)
{
if (key != null)
{
writer.WritePropertyName(key);
}
JsonSerializer.Serialize(writer, value, options);
}
}
測試程式碼如下:
- 先建立 Dictionary<string, object>
- 序列化成 Json 文字
- 使用 DictionaryStringObjectJsonConverter 反序列化 Dictionary<string, object>
[TestMethod]
public void 字串轉Dictionary()
{
var options = new JsonSerializerOptions
{
Converters = { new DictionaryStringObjectJsonConverter() }
};
var expected = new Dictionary<string, object>
{
["anonymousType"] = new { Prop = 123 },
["model"] = new Model { Age = 19, Name = "yao" },
["null"] = null!,
["dateTimeOffset"] = DateTimeOffset.Now,
["long"] = (long)255,
["decimal"] = (decimal)3.1416,
["guid"] = Guid.NewGuid(),
["string"] = "String",
["decimalArray"] = new[] { 1, (decimal)2.1 },
};
var json = JsonSerializer.Serialize(expected, options);
var actual = JsonSerializer.Deserialize<Dictionary<string, object>>(json, options);
Assert.AreEqual(expected["dateTimeOffset"], actual["dateTimeOffset"]);
Assert.AreEqual(expected["string"], actual["string"]);
Assert.AreEqual(expected["long"], actual["long"]);
Assert.AreEqual(expected["decimal"], actual["decimal"]);
Assert.AreEqual(expected["null"], actual["null"]);
AssertAnonymousType(actual["anonymousType"] as Dictionary<string, object>);
AssertDecimalArray(actual["decimalArray"] as List<object>);
}
private static void AssertAnonymousType(Dictionary<string, object> actual)
{
var expected = new Dictionary<string, object>
{
{ "Prop", (long)123 }
};
Assert.AreEqual(expected["Prop"], actual["Prop"]);
}
private static void AssertDecimalArray(List<object> actual)
{
var expected = new List<object>
{
(long)1,
(decimal)2.1
};
Assert.AreEqual(expected[0], actual[0]);
Assert.AreEqual(expected[1], actual[1]);
}
JsonDocument 反序列化 <Dictionary<string, object>>
To<T> 方法骨子裡是調用 JsonDocument.Deserialize<T>(options),沒有甚麼好說嘴的,除了 To 方法之外還有一些我常用的互轉,提供給各位參考
public static class JsonDocumentExtensions
{
public static T To<T>(this JsonDocument source,
JsonSerializerOptions options = default)
{
return source.Deserialize<T>(options);
}
public static JsonDocument ToJsonDocument<T>(this T source,
JsonDocumentOptions options = default)
where T : class
{
return JsonDocument.Parse(JsonSerializer.SerializeToUtf8Bytes(source), options);
}
public static JsonDocument ToJsonDocument(this string source,
JsonDocumentOptions options = default)
{
return JsonDocument.Parse(source, options);
}
public static string ToJsonString(this JsonDocument source,
JsonWriterOptions options = default)
{
if (source == null)
{
return null;
}
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
source.WriteTo(writer);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
}
測試代碼如下
[TestMethod]
public void JsonDocument轉Dictionary()
{
var options = new JsonSerializerOptions
{
Converters = { new DictionaryStringObjectJsonConverter() }
};
var expected = new Dictionary<string, object>
{
["anonymousType"] = new { Prop = 123 },
["model"] = new Model { Age = 19, Name = "yao" },
["null"] = null!,
["dateTimeOffset"] = DateTimeOffset.Now,
["long"] = (long)255,
["decimal"] = (decimal)3.1416,
["guid"] = Guid.NewGuid(),
["string"] = "String",
["decimalArray"] = new[] { 1, (decimal)2.1 },
};
var json = JsonSerializer.Serialize(expected);
using var jsonDoc = json.ToJsonDocument();
var actual = jsonDoc.To<Dictionary<string, object>>(options);
Assert.AreEqual(expected["dateTimeOffset"], actual["dateTimeOffset"]);
Assert.AreEqual(expected["string"], actual["string"]);
Assert.AreEqual(expected["long"], actual["long"]);
Assert.AreEqual(expected["decimal"], actual["decimal"]);
Assert.AreEqual(expected["null"], actual["null"]);
AssertAnonymousType(actual["anonymousType"] as Dictionary<string, object>);
AssertDecimalArray(actual["decimalArray"] as List<object>);
}
JsonNode 反序列化 <Dictionary<string, object>>
反序列化的用法跟 JsonDocument 一樣,就不多做解釋了
public static class JsonNodeExtensions
{
public static T To<T>(this JsonNode source,
JsonSerializerOptions options = default)
{
return source.Deserialize<T>(options);
}
public static JsonNode ToJsonNode<T>(this T source,
JsonNodeOptions options = default)
where T : class
{
return JsonNode.Parse(JsonSerializer.SerializeToUtf8Bytes(source), options);
}
public static JsonNode ToJsonNode(this string source,
JsonNodeOptions options = default)
{
return JsonNode.Parse(source, options);
}
public static string ToJsonString(this JsonNode source,
JsonWriterOptions options = default)
{
if (source == null)
{
return null;
}
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
source.WriteTo(writer);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
}
測試程式碼如下:
[TestMethod]
public void JsonsNode轉Dictionary()
{
var options = new JsonSerializerOptions
{
Converters = { new DictionaryStringObjectJsonConverter() }
};
var expected = new Dictionary<string, object>
{
["anonymousType"] = new { Prop = 123 },
["model"] = new Model { Age = 19, Name = "小章" },
["null"] = null!,
["dateTimeOffset"] = DateTimeOffset.Now,
["long"] = (long)255,
["decimal"] = (decimal)3.1416,
["guid"] = Guid.NewGuid(),
["string"] = "字串",
["decimalArray"] = new[] { 1, (decimal)2.1 },
};
var json = JsonSerializer.Serialize(expected);
var jsonObject = json.ToJsonNode();
var actual = jsonObject.To<Dictionary<string, object>>(options);
Assert.AreEqual(expected["dateTimeOffset"], actual["dateTimeOffset"]);
Assert.AreEqual(expected["string"], actual["string"]);
Assert.AreEqual(expected["long"], actual["long"]);
Assert.AreEqual(expected["decimal"], actual["decimal"]);
Assert.AreEqual(expected["null"], actual["null"]);
AssertAnonymousType(actual["anonymousType"] as Dictionary<string, object>);
AssertDecimalArray(actual["decimalArray"] as List<object>);
}
參考資料
如何在中使用 JSON 檔、Utf8JsonReader 和 Utf8JsonWriter System.Text.Json | Microsoft Docs
Custom Dictionary JsonConverter using System.Text.Json (josef.codes)
.NET Core JSON 轉 Dictionary string, object 地雷-黑暗執行緒 (darkthread.net)
範例位置
sample.dotblog/Json/Lab.JsonConverter at master · yaochangyu/sample.dotblog (github.com)
後記
後來發現 ReadValue 在處理 Json Object 時轉成匿名型別好像不是很好處理比對,所以改轉成 Dictionary<string,object>,關鍵在下圖,其餘的動作就跟上面的一樣
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET