当我们使用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. Hive-服务启动和停止命令

    1.启动命令 #!/bin/bash nohup hive --service metastore >> $HIVE_HOME/logs/metasotre.log 2>&1 ...

  2. 金仓数据库(kingbase)的system用户被锁的解锁步骤

    今天发现我安装的金仓数据库(kingbase)的system用户被锁了,无法登陆.百度搜了下,使用SUPERMANAGER_V8ADMIN可以解锁,试了下果然如此.解锁步骤如下: 用户:SUPERMA ...

  3. 如何使用graalvm为带有反射功能的java代码生成native image

    译自Configure Native Image with the Tracing Agent graal官方文档 , 以下所有命令需要在linux环境下操作,graalvm也支持windows. 要 ...

  4. VMware 虚拟机一键去虚拟化工具

    前言: 如果你想在 VMware 虚拟机里面多开玩游戏的话,但是现在大多数网游都会检测是否虚拟机,进入游戏被检测到在虚拟机中运行,游戏可能直接闪退.所以就得对 VMware 虚拟机进行去除虚拟化. 原 ...

  5. Linux进程通信 | 信号

    一.什么是信号? 信号就像是一个突然的电话铃声,它会打断正在进行的程序并引起其注意. 在Linux系统中,信号是一种软件中断,它通常是异步发生的,可以用来通知进程某个事件已经发生..每个信号都有一个唯 ...

  6. JVM详解

    1 JVM运行机制概述 JVM运行机制 类加载机制: 类加载过程由类加载器来完成,即由ClassLoader及其子类实现,有隐式加载和显式加载两种方式.隐式加载是指在使用new等方式创建对象时会隐式调 ...

  7. 微信小程序云开发项目-个人待办事项-03【主页】模块开发

    上一篇: 微信小程序云开发项目-个人待办事项-02今日模块开发 https://blog.csdn.net/IndexMan/article/details/124497893 模块开发步骤 本篇介绍 ...

  8. Oracle使用由字符串索引的二维数组

    –参考文章:http://www.oracle.com/technetwork/issue-archive/2014/14-sep/o54plsql-2245345.html –SAMPLE DATA ...

  9. std::shared_ptr 和 std::vector 的结合使用

    #include <iostream> #include <string> #include <vector> std::shared_ptr<std::ve ...

  10. 【Android 抓包对抗】客户端证书和域名校验绕过

    1. 按照之前的方式(https://www.cnblogs.com/gradyblog/p/17197707.html)进行抓包发现证书校验失败 SSL handshake with client ...