發現從 .NET 6 開始支援 System.Text.Json DOM 巡覽及編修,小小興奮了一下,打算逐步用 System.Text.Json 取代 Json.NET,不料隨即踩到雷。

有段用 JSON 傳送 Dictionary<string, object> 的程式,原本靠 JsonConvert.DeserializeObject<Dictionary<string, object>>() 將 JSON 轉回 Dictionary<string, object>。開開心心改成 JsonSerializer.Deserializ <Dictionary<string, object>>(),結果在跑測試時爆炸!

用以下程式重現問題:

using System.Text.Json;

var jsonOpt = new JsonSerializerOptions {
    WriteIndented = true
};
var dict = new Dictionary<string, object>() {
    ["i"] = 255,
    ["s"] = "String",
    ["d"] = DateTime.Today,
    ["a"] = new int[] { 1, 2 },
    ["o"] = new { Prop = 123 }
};
var json = JsonSerializer.Serialize(dict, jsonOpt);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("== JSON ==");
Console.WriteLine(json);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("== Json.NET ==");
var dJsonNet = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
foreach (var kv in dJsonNet!) {
    Console.WriteLine($"{kv.Value.GetType().Name} {kv.Key} = {kv.Value}");
}
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("== System.Text.Json ==");
var dSysTextJson = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
foreach (var kv in dSysTextJson!) {
    Console.WriteLine($"{kv.Value.GetType().Name} {kv.Key} = {kv.Value}");
}
Console.ResetColor();

問題點為 System.Text.Json 在反序列化 object 時不像 Json.NET 會試著轉型成 int、string、DateTime 等型別(我另外加測了 int[] 及物件,Json.NET 會轉成 JArray 及 JObject),而是一律視為 JsonElement,雖然 JsonElement 提供了 GetByte()、GetGuid()、GetInt32()、GetDateTime()... 等方法,不愁取不出值,但得逐 Key 分別處理,應用上不如 Dictionary<string, object> 便利。

爬文查到有位微軟 MVP 分享自製 DictionaryStringObjectJsonConverter 的解法,透過 JsonConverterAttribute 套用在類別屬性上,原則上可以克服問題。但我心中更理想的解法是未來 System.Text.Json 能內建選項,提供與 Json.NET 類似的轉換邏輯,在此之前,我先試寫一個簡易版擴充函式 ToStringObjectDictionary() 頂著用,順便練手感。

using System.Text.Json.Nodes;
namespace System.Text.Json
{
    public static class JsonDictStringObjExtensions
    {
        public static Dictionary<string, object> ToStringObjectDictionary(this JsonObject jsonObject)
        {
            var dict = new Dictionary<string, object>();
            foreach (var prop in jsonObject) 
            {
                object value;
                if (prop.Value == null) value = null!;
                else if (prop.Value is JsonArray) value = prop.Value.AsArray();
                else if (prop.Value is JsonObject) value = prop.Value.AsObject();
                else 
                {
                    var v = prop.Value.AsValue();
                    var t = prop.Value.ToJsonString();
                    if (t.StartsWith('"')) {
                        if (v.TryGetValue<DateTime>(out var d)) value = d;
                        else if (v.TryGetValue<Guid>(out var g)) value = g;
                        else value = v.GetValue<string>();
                    }
                    else value = v.GetValue<decimal>();
                }
                dict.Add(prop.Key, value);
            }
            return dict;
        }
    }
}

修改測試程式,再多測試浮點數、Guid 及 null:

using System.Text.Json;
using System.Text.Json.Nodes;

var jsonOpt = new JsonSerializerOptions {
    WriteIndented = true
};
var dict = new Dictionary<string, object>() {
    ["i"] = 255,
    ["f"] = 3.1416,
    ["s"] = "String",
    ["d"] = DateTime.Today,
    ["a"] = new int[] { 1, 2 },
    ["o"] = new { Prop = 123 },
    ["g"] = Guid.NewGuid(),
    ["n"] = null!
};
var json = JsonSerializer.Serialize(dict, jsonOpt);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("== JSON ==");
Console.WriteLine(json);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("== Json.NET ==");
var dJsonNet = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
foreach (var kv in dJsonNet!) {
    Console.WriteLine($"{kv.Value?.GetType().Name} {kv.Key} = {kv.Value ?? "null"}");
}
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("== System.Text.Json ==");
var dSysTextJson = JsonSerializer.Deserialize<JsonObject>(json)!.ToStringObjectDictionary();
foreach (var kv in dSysTextJson!) {
    Console.WriteLine($"{kv.Value?.GetType().Name} {kv.Key} = {kv.Value ?? "null"}");
}
Console.ResetColor();

測試成功!

When deserializing Dictionary<string, object> with System.Text.Json, object value will be JsonElement, not as convient as Json.NET. This article reveal this issue and provide some workaround.


Comments

Be the first to post a comment

Post a comment