5

.NET Core JSON 轉 Dictionary string, object 地雷

 2 years ago
source link: https://blog.darkthread.net/blog/dotnet-json-deserialize-dictionary-issue/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
.NET Core JSON 轉 Dictionary string, object 地雷-黑暗執行緒

發現從 .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();

測試成功!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK