• 自定义Key类型的字典无法序列化的N种解决方案


    当我们使用System.Text.Json.JsonSerializer对一个字典对象进行序列化的时候,默认情况下字典的Key不能是一个自定义的类型,本文介绍几种解决方案。

    一、问题重现
    二、自定义JsonConverter能解决吗?
    三、自定义TypeConverter能解决问题吗?
    四、以键值对集合的形式序列化
    五、转换成合法的字典
    六、自定义读写

    一、问题重现

    我们先通过如下这个简单的例子来重现上述这个问题。如代码片段所示,我们定义了一个名为Point(代表二维坐标点)的只读结构体作为待序列化字典的Key。Point可以通过结构化的表达式来表示,我们同时还定义了Parse方法将表达式转换成Point对象。

    复制代码
    using System.Diagnostics;
    using System.Text.Json;
    
    var dictionary = new Dictionaryint>
    {
        { new Point(1.0, 1.0), 1 },
        { new Point(2.0, 2.0), 2 },
        { new Point(3.0, 3.0), 3 }
    };
    
    try
    {
        var json = JsonSerializer.Serialize(dictionary);
        Console.WriteLine(json);
    
        var dictionary2 = JsonSerializer.Deserializeint>>(json)!;
        Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
        Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
        Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    
    
    public readonly record struct Point(double X, double Y)
    {
        public override string ToString()=> $"({X}, {Y})";
        public static Point Parse(string s)
        {
            var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
            if (tokens.Length != 2)
            {
                throw new FormatException("Invalid format");
            }
            return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
        }
    }
    复制代码

    当我们使用JsonSerializer序列化多一个Dictionaryint>类型的对象时,会抛出一个NotSupportedException异常,如下所示的信息解释了错误的根源:Point类型不能作为被序列化字典对象的Key。顺便说一下,如果使用Newtonsoft.Json,这样的字典可以序列化成功,但是反序列化会失败。

    image

    二、自定义JsonConverter能解决吗?

    遇到这样的问题我们首先想到的是:既然不执行针对Point的序列化/反序列化,那么我们可以对应相应的JsonConverter自行完成序列化/反序列化工作。为此我们定义了如下这个PointConverter,将Point的表达式作为序列化输出结果,同时调用Parse方法生成反序列化的结果。

    public class PointConverter : JsonConverter
    {
        public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)=> Point.Parse(reader.GetString()!);
        public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
    }

    我们将这个PointConverter对象添加到创建的JsonSerializerOptions配置选项中,并将后者传入序列化和反序列化方法中。

    复制代码
    var options = new JsonSerializerOptions
    {
        WriteIndented = true,
        Converters = { new PointConverter() }
    };
    var json = JsonSerializer.Serialize(dictionary, options);
    Console.WriteLine(json);
    
    var dictionary2 = JsonSerializer.Deserializeint>>(json, options)!;
    Debug.Assert(dictionary2[new Point(1.0, 1.0)] == 1);
    Debug.Assert(dictionary2[new Point(2.0, 2.0)] == 2);
    Debug.Assert(dictionary2[new Point(3.0, 3.0)] == 3);
    复制代码

    不幸的是,这样的解决方案无效,序列化时依然会抛出相同的异常。

    image

    三、自定义TypeConverter能解决问题吗?

    JsonConverter的目的本质上就是希望将Point对象视为字符串进行处理,既然自定义JsonConverter无法解决这个问题,我们是否可以注册相应的类型转换其来解决它呢?为此我们定义了如下这个PointTypeConverter 类型,使它来完成针对Point和字符串之间的类型转换。

    复制代码
    public class PointTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string);
        public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) => destinationType == typeof(string);
        public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => Point.Parse((string)value);
        public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) => value?.ToString()!;
    }
    复制代码

    我们利用标注的TypeConverterAttribute特性将PointTypeConverter注册到Point类型上。

    复制代码
    [TypeConverter(typeof(PointTypeConverter))]
    public readonly record struct Point(double X, double Y)
    {
        public override string ToString() => $"({X}, {Y})";
        public static Point Parse(string s)
        {
            var tokens = s.Trim('(',')').Split(',', StringSplitOptions.TrimEntries);
            if (tokens.Length != 2)
            {
                throw new FormatException("Invalid format");
            }
            return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
        }
    }
    
    
    复制代码

    实验证明,这种解决方案依然无效,序列化时还是会抛出相同的异常。顺便说一下,这种解决方案对于Newtonsoft.Json是适用的。

    image

    四、以键值对集合的形式序列化

    为Point定义JsonConverter之所以不能解决我们的问题,是因为异常并不是在试图序列化Point对象时抛出来的,而是在在默认的规则序列化字典对象时,不合法的Key类型没有通过验证。如果希望通过自定义JsonConverter的方式来解决,目标类型不应该时Point类型,而应该时字典类型,为此我们定义了如下这个PointKeyedDictionaryConverter类型。

    我们知道字典本质上就是键值对的集合,而集合针对元素类型并没有特殊的约束,所以我们完全可以按照键值对集合的方式来进行序列化和反序列化。如代码把片段所示,用于序列化的Write方法中,我们利用作为参数的JsonSerializerOptions 得到针对IEnumerable>类型的JsonConverter,并利用它以键值对的形式对字典进行序列化。

    复制代码
    public class PointKeyedDictionaryConverter : JsonConverter>
    {
        public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var enumerableConverter = (JsonConverter>>)options.GetConverter(typeof(IEnumerable>));
            return enumerableConverter.Read(ref reader, typeof(IEnumerable>), options)?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        }
        public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)
        {
            var enumerableConverter = (JsonConverter>>)options.GetConverter(typeof(IEnumerable>));
            enumerableConverter.Write(writer, value, options);
        }
    }
    复制代码

    用于反序列化的Read方法中,我们采用相同的方式得到这个针对IEnumerable>类型的JsonConverter,并将其反序列化成键值对集合,在转换成返回的字典。

    var options = new JsonSerializerOptions
    {
        WriteIndented = true,
        Converters = { new PointConverter(), new PointKeyedDictionaryConverter<int>()}
    };

    我们将PointKeyedDictionaryConverter<int>添加到创建的JsonSerializerOptions配置选项的JsonConverter列表中。从如下所示的输出结果可以看出,我们创建的字典确实是以键值对集合的形式进行序列化的。

    image

    五、转换成合法的字典

    既然作为字典Key的Point可以转换成字符串,那么可以还有另一种解法,那就是将以Point为Key的字典转换成以字符串为Key的字典,为此我们按照如下的方式重写的PointKeyedDictionaryConverter。如代码片段所示,重写的Writer方法利用传入的JsonSerializerOptions配置选项得到针对Dictionary的JsonConverter,然后将待序列化的Dictionary 对象转换成Dictionary 交给它进行序列化。

    复制代码
    public class PointKeyedDictionaryConverter : JsonConverter>
    {
        public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var converter = (JsonConverterstring, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
            return converter.Read(ref reader, typeof(Dictionary<string, TValue>), options)
                ?.ToDictionary(kv => Point.Parse(kv.Key), kv=> kv.Value);
        }
        public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)
        {
            var converter = (JsonConverterstring, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
            converter.Write(writer, value.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options);
        }
    }
    复制代码

    重写的Read方法采用相同的方式得到JsonConverterstring, TValue>>对象,并利用它执行反序列化生成Dictionary 对象。我们最终将它转换成需要的Dictionary 对象。从如下所示的输出可以看出,这次的序列化生成的JSON会更加精炼,因为这次是以字典类型输出JSON字符串的。

    image

    六、自定义读写

    虽然以上两种方式都能解决我们的问题,而且从最终JSON字符串输出的长度来看,第二种具有更好的性能,但是它们都有一个问题,那么就是需要创建中间对象。第一种方案需要创建一个键值对集合,第二种方案则需要创建一个Dictionary 对象,如果对性能有更高的追求,它们都不是一种好的解决方案。既让我们都已经在自定义JsonConverter,完全可以自行可控制JSON内容的读写,为此我们再次重写了PointKeyedDictionaryConverter

    复制代码
    public class PointKeyedDictionaryConverter : JsonConverter>
    {
        public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            JsonConverter? valueConverter = null;
            Dictionary? dictionary = null;
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return dictionary;
                }
                valueConverter ??= (JsonConverter)options.GetConverter(typeof(TValue))!;
                dictionary ??= [];
                var key = Point.Parse(reader.GetString()!);
                reader.Read();
                var value = valueConverter.Read(ref reader, typeof(TValue), options)!;
                dictionary.Add(key, value);
            }
            return dictionary;
        }
        public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            JsonConverter? valueConverter = null;
            foreach (var (k, v) in value)
            {
                valueConverter ??= (JsonConverter)options.GetConverter(typeof(TValue))!;
                writer.WritePropertyName(k.ToString());
                valueConverter.Write(writer, v, options);
            }
            writer.WriteEndObject();
        }
    }
    复制代码

    如上面的代码片段所示,在重写的Write方法中,我们调用Utf8JsonWriter 的WriteStartObject和 WriteEndObject方法以对象的形式输出字典。在这中间,我们便利字典的每个键值对,并以“属性”的形式对它们进行输出(Key和Value分别是属性名和值)。在Read方法中,我们创建一个空的Dictionary 对象,在一个循环中利用Utf8JsonReader先后读取作为Key的字符串和Value值,最终将Key转换成Point类型,并添加到创建的字典中。从如下所示的输出结果可以看出,这次生成的JSON具有与上面相同的结构。

    image

  • 相关阅读:
    自动化运维工具Ansible工具学习及使用---持续更新中
    ROS零散知识点
    Redis 面试题
    第十九章《类的加载与反射》第2节:类加载器
    JavaEE:File类查询一个文件的路径(举例+源码 )
    VS工程的“多dll与exe文件合并”
    第七章:最新版零基础学习 PYTHON 教程—Python 列表(第二节 -Python 创建具有给定范围的数字列表)
    单片机通用学习-什么是时钟?
    React 重${}【ES6 引入了模板字符串解决这个问题】
    ThreadLocal 核心源码分析
  • 原文地址:https://www.cnblogs.com/artech/p/18075402/dictionary_key_serialization