YM

Back

很容易发现的技巧#

以下基本操作不过多赘述:

使用带参数的构造函数/反序列化只读属性#

默认情况下,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
plaintext

JsonConverter 类可以提供给 JsonSerializerOptions.Converters,用于自定义转换格式。ObjectConverter 类可以将 Object 类型转换为一个 Type 字段和一个 Value 字段,也可以通过这两个字段反过来转换到对应的类型。

当然,这里的 Type 字段的内容也要经过适当的加密处理,才能投入到正常使用中。

System.Text.Json 的一些使用技巧
https://yanming.link/blog/system-text-json-tricks
Author YM
Published at October 14, 2024