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

一、问题重现

二、自定义JsonConverter能解决吗?

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

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

五、转换成合法的字典

六、自定义读写

一、问题重现

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

using System.Diagnostics;
using System.Text.Json; var dictionary = new Dictionary<Point, int>
{
{ 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.Deserialize<Dictionary<Point, int>>(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.Split(',', StringSplitOptions.TrimEntries);
if (tokens.Length != 2)
{
throw new FormatException("Invalid format");
}
return new Point(double.Parse(tokens[0]), double.Parse(tokens[1]));
}
}

当我们使用JsonSerializer序列化多一个Dictionary<Point, int>类型的对象时,会抛出一个NotSupportedException异常,如下所示的信息解释了错误的根源:Point类型不能作为被序列化字典对象的Key。

二、自定义JsonConverter能解决吗?

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

public class PointConverter : JsonConverter<Point>
{
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.Deserialize<Dictionary<Point, int>>(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);

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

三、自定义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]));
}
}

实验证明,这种解决方案依然无效,序列化时还是会抛出相同的异常。

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

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

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

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

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

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

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

五、转换成合法的字典

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

public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var converter = (JsonConverter<Dictionary<string, 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<Point, TValue> value, JsonSerializerOptions options)
{
var converter = (JsonConverter<Dictionary<string, TValue>>)options.GetConverter(typeof(Dictionary<string, TValue>))!;
converter.Write(writer, value.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options);
}
}

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

六、自定义读写

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

public class PointKeyedDictionaryConverter<TValue> : JsonConverter<Dictionary<Point, TValue>>
{
public override Dictionary<Point, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
JsonConverter<TValue>? valueConverter = null;
Dictionary<Point, TValue>? dictionary = null;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return dictionary;
}
valueConverter ??= (JsonConverter<TValue>)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<Point, TValue> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
JsonConverter<TValue>? valueConverter = null;
foreach (var (k, v) in value)
{
valueConverter ??= (JsonConverter<TValue>)options.GetConverter(typeof(TValue))!;
writer.WritePropertyName(k.ToString());
valueConverter.Write(writer, v, options);
}
writer.WriteEndObject();
}
}

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

自定义Key类型的字典无法序列化的N种解决方案的更多相关文章

  1. C++之自定义key类型,重载操作符

    #include <map>#include <string>using namespace std;class MyString{ public:MyString(){m_s ...

  2. c/c++ 标准库 set 自定义关键字类型与比较函数

    标准库 set 自定义关键字类型与比较函数 问题:哪些类型可以作为标准库set的关键字类型呢??? 答案: 1,任意类型,但是需要额外提供能够比较这种类型的比较函数. 2,这种类型实现了 < 操 ...

  3. 遍历两个自定义列表来实现字典(key:value)

    #自定义key    ${keys}    create list    key1    key2    key3 #自定义value    ${values}    create list    v ...

  4. [C#] 类型学习笔记三:自定义值类型

    既前两篇之后,这一篇我们讨论通过struct 关键字自定义值类型. 在第一篇已经讨论过值类型的优势,节省空间,不会触发Gargage Collection等等. 在对性能要求比较高的场景下,通过str ...

  5. c#关于Dictionary中自定义Key

    Dictionary 描述 字典 Dictionary 通过 Hash 桶算法进行O(1)查找数据,在 Hash 碰撞达到一定次数后会自动进行 Resize,也会在数组大小不足的时候会自动进行Resi ...

  6. 不要使用Integer做HashMap的key,尤其在json序列化的时候

    使用redisson cache来实现一个缓存功能,缓存省市县的名称,key是区域编码,integer,value是name.结果取的时候,怎么都取不出. Map<Integer, String ...

  7. Zabbix 自定义Key

    系统:Linux Centos 7.4 x64.Windos 2008 x64 服务:Zabbix 3.0.16 说明1:自定义Key 主要通过自定义 脚本 或者 命令 来实现自定义监控类型,需要在a ...

  8. Qt5 QtQuick系列----QtQuick的Secne Graph剖析(2)--自定义QML类型 (继承QQuickItem)

    "当下即永恒"  --- 佚名 Qt用户可以方便地使用QML中的Rectangle等基本类型,但是当不够用时,或,需要开发更高级的界面时,可以自己定义QML类型. 自定义QML类型 ...

  9. zabbix active模式以及自定义key not Supported的解决

    zabbix active模式 active模式适用场景 zabbix server端无法直连agent端,比如agent为内网机器,仅有内网ip,没有公网ip,但是内网机器能够访问server端 a ...

  10. spring-redis-session 自定义 key 和过期时间

    对于分布式应用来说,最开始遇到的问题就是 session 的存储了,解决方案大致有如下几种 使用 spring-session 它可以把 session 存储到你想存储的位置,如 redis,mysq ...

随机推荐

  1. 在.NET Core下的机器学习--学习笔记

    摘要 .NET Core 在机器学习的应用场景,除了 ML .NET 还会介绍一个非常棒的開源技術 TensorFlow .NET , Keras .NET. 讲师介绍 本课内容 人工智能介绍 ML ...

  2. 使用OBS Studio软件进行桌面录屏

    操作系统 :Windows10_x64 OBS Studio是开源免费的录屏和直播软件,支持Windows.macOS及Linux操作系统. 这里记录下桌面录屏和桌面区域录屏的使用,也方便我后续查阅( ...

  3. k8s-权限管理

    目录 1. 身份认证 node节点操作 创建普通用户并授权 1. 生成私钥 2. 生成zhangsan用户证书请求文件 3. 为zhangsan用户颁发证书 4. 创建命名空间及pod 5. 创建角色 ...

  4. Shiro实战1-介绍

    什么是 Shiro 官网:http://shiro.apache.org/ shiro是一款主流的 Java 安全框架,不依赖任何容器,可以运行在 Java SE和 Java EE 项目中,它的主要作 ...

  5. vue+element-ui项目搭建实战

    1.使用vue ui创建vue工程 利用vue-cli提供的图形化工具快速搭建vue工程: 命令行运行:vue ui 工程结构说明 build:项目构建webpack(打包器)相关代码 config: ...

  6. 我的小程序之旅五:微信公众号扫码登录PC端网页

    代码仓库:https://gitee.com/wlovet/gzh-qrlogin 一.准备材料 1.已认证的公众号(必须为服务号,订阅号没有该接口的权限) 2.一个网址,用于微信回调,推荐一个内网穿 ...

  7. win32 - MultiByteToWideChar的示例

    该函数经常被用来处理UTF-8和ANSI格式的字符串,将它们转换为宽字节(UTF-16) #include <iostream> #include <Windows.h> #i ...

  8. MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2

    描述: 2021-08-xx 13:31:30.049 DEBUG 9208 : ==> Preparing: SELECT SUM(end_vt) - SUM(start_vt) FROM s ...

  9. 亲测可行,Android Studio 查看源码出现 Source for ‘Android API xxx Platform’ not found 的解决方法

    亲测可行,Android Studio 查看源码出现 Source for 'Android API xxx Platform' not found 的解决方法 如标题中的问题,产生的原因就是 SDK ...

  10. signature hdr data: BAD, no. of btyes(9088) out of range 问题排查与解决方案

    在使用yum工具安装gcc的时候,报出了signature hdr data: BAD, no. of btyes(9088) out of range 的问题 这是由于centos8中rpm工具存在 ...