[.NET 6] 自訂 JsonConverter 反序列化 Dictionary<string, object>

我使用預設的 System.Text.Json 反序列化時 JsonSerializer.Deserialize<Dictionary<string, object>>(json),得到 JsonElement,再透過 JsonElement.Get 系列的方法才能取得正確的資料,這樣有點繁瑣,為此我找到了解方,自行實作 JsonConverter,緊接著,來看看我怎麼處理的

前言

.NET 6 替 System.Text.Json 新增 DOM 的控制物件:JsonNodeJsonArrayJsonObjectJsonValue  這可以更有效率的處理 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);
    }
}

 

測試程式碼如下:

  1. 先建立 Dictionary<string, object>
  2. 序列化成 Json 文字
  3. 使用 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>);
}

 

參考資料

.NET 6 的新功能 | Microsoft Docs

如何在中使用 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>,關鍵在下圖,其餘的動作就跟上面的一樣

實作
sample.dotblog/DictionaryStringObjectJsonConverter2.cs at master · yaochangyu/sample.dotblog (github.com)

測試
sample.dotblog/DictionaryStringObjectJsonConverter2Tests.cs at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo