很容易发现的技巧#
以下基本操作不过多赘述:
使用带参数的构造函数/反序列化只读属性#
默认情况下,
System.Text.Json
使用默认的公共无参数构造函数。 但可以让其使用参数化构造函数,该构造函数可以反序列化不可变的类或结构。
对于类,如果唯一构造函数是参数化的构造函数,则将使用该构造函数。
对于结构或包含多个构造函数的类,通过应用 [JsonConstructor ↗] 特性来指定要使用的构造函数。 如果未使用该特性,则始终使用公共无参数构造函数(如果存在)。
使用 JsonConstructorAttribute
可以在反序列化时指定要调用的构造函数,来达到对数据进行额外处理,或者在构造函数中对只读属性的赋值目的。
class PersonInformation
{
public string Name { get; set; }
public int Id { get; }
public PersonInformation(string name)
{
Name = name;
Id = CreateNew(name);
}
[JsonConstructor]
private PersonInformation(int id)
{
var p = GetPersonInformationById(id);
Name = p.Name;
}
public static PersonInformation GetPersonInformationById(int id)
{
// 根据 ID 拿到用户
return new PersonInformation("unknown");
}
private int CreateNew(string name)
{
// 申请新用户 ID,并将 name 存进数据库里面
return 0;
}
}
csharp上面的示例是 JsonConstructorAttribute
的其中一种应用(虽然好像这个类的设计有问题),如果 PersonInformation
类存储的不只是名字,还有其它更多的信息,而这些信息恰好其实也存在数据库中,可以通过 ID 查询得到,就不需要将所有属性都存起来了,只需要存一个 ID 即可。而调用 PersonInformation(string name)
则会直接在数据库里面创建一个新的用户,不应该在反序列化时调用,所以制定 PersonInformation(int id)
为反序列化时调用的构造函数。
在下面的序列化 Type 类型里面,也有用到 JsonConstructorAttribute
。
序列化 Type 类型#
如果尝试序列化 Type 类型:
using System.Text.Json;
var t = new MyType(typeof(int));
JsonSerializer.Serialize(t);
record MyType(Type Value);
csharp则会引发 System.NotSupportedException
异常:Serialization and deserialization of ‘System.Type’ instances is not supported. Path: $.Value.”.
原因是直接序列化 Type
类型,然后再反序列化会使得程序容易受到攻击。攻击者可以通过 Type
类型字段窥探到程序内部结构甚至修改这个字段以达到攻击目的。
如果还是想保存 Type 类型怎么办呢?自己实现就好啦,有两种方法:
- 简单点的,修改类型,增加一个非
Type
类型的用于保存Type
信息的字段 - 麻烦点的,使用自定义转换器
JsonConverter
类
这里先介绍第一种简单点的方法,可以作为只有一两个类需要用到的临时解决方案,缺点是要序列化的带 Type
类型的每个类都需要新增字段,极其不优雅。第二种方法可以使用下一节的 JsonConverter
来做。
将以上 MyType
类型修改为:
class MyType
{
[JsonIgnore]
public Type Value { get; set; }
public string ValueString
{
get => Value.AssemblyQualifiedName ?? Value.FullName ?? Value.Name;
[MemberNotNull(nameof(Value))]
set => Value = Type.GetType(value) ?? throw new ArgumentException($"无法根据字符串 {value} 找到类型!", nameof(value));
}
public MyType(Type value)
{
Value = value;
}
[JsonConstructor]
public MyType(string valueString)
{
ValueString = valueString;
}
}
csharp以上示例代码没有考虑安全问题,直接使用这个方法,仍然不会解决序列化类型对象的安全问题!如果你不信任这个软件的使用者,起码也要对 ValueString
进行加密处理。
ValueString.Get
方法先获取的 AssemblyQualifiedName
是因为兼容跨程序集的类型使用,如果你的这个类并不会使用其它项目的类型的话,直接使用 FullName
就可以了。要注意的是并不可以通过 Type.Name
得到的字符串,来传递给 Type.GetType()
来获取到类型对象。
反序列化 Object 类型的字段#
假设有一个类,它有一个 object
类型的属性,用来装箱任何类型:
record MyObject(object Obj);
csharp然后有以下代码:
var o = new MyObject("blog.yanming.link");
Console.WriteLine($"序列化前,Obj 类型: {o.Obj.GetType()}");
var s = JsonSerializer.Serialize(o);
Console.WriteLine($"序列化的 Json: {s}");
var o2 = JsonSerializer.Deserialize<MyObject>(s)!;
Console.WriteLine($"反序列化后,Obj 类型: {o2.Obj.GetType()}");
csharp调用后将会输出的是:
序列化前,Obj 类型: System.String
序列化的 Json: {"Obj":"blog.yanming.link"}
反序列化后,Obj 类型: System.Text.Json.JsonElement
plaintext啊哦,System.String
类型变成了 System.Text.Json.JsonElement
类型。
明显是反序列化的时候,不知道要反序列化成什么类型,这时候我们就是使用 JsonConverter
自定义序列化时候的行为,在序列化时给 Object 类型的对象加上它们原本的类型信息。
可以定义一个这样子的类:
public class ObjectConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var doc = JsonDocument.ParseValue(ref reader);
var type = Type.GetType(doc.RootElement.GetProperty("Type").GetString()!)!;
var obj = JsonSerializer.Deserialize(doc.RootElement.GetProperty("Value").GetRawText(), type, options)!;
return obj;
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("Type", value.GetType().AssemblyQualifiedName);
writer.WritePropertyName("Value");
JsonSerializer.Serialize(writer, value, value.GetType(), options);
writer.WriteEndObject();
}
}
csharp然后如此使用即可:
var option = new JsonSerializerOptions
{
Converters = { new ObjectConverter() }
};
var o = new MyObject("blog.yanming.link");
Console.WriteLine($"序列化前,Obj 类型: {o.Obj.GetType()}");
var s = JsonSerializer.Serialize(o, option);
Console.WriteLine($"序列化的 Json: {s}");
var o2 = JsonSerializer.Deserialize<MyObject>(s, option)!;
Console.WriteLine($"反序列化后,Obj 类型: {o2.Obj.GetType()}");
csharp最后的输出是:
序列化前,Obj 类型: System.String
序列化的 Json: {"Obj":{"Type":"System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e","Value":"blog.yanming.link"}}
反序列化后,Obj 类型: System.String
plaintextJsonConverter
类可以提供给 JsonSerializerOptions.Converters
,用于自定义转换格式。ObjectConverter
类可以将 Object
类型转换为一个 Type
字段和一个 Value
字段,也可以通过这两个字段反过来转换到对应的类型。
当然,这里的 Type
字段的内容也要经过适当的加密处理,才能投入到正常使用中。