【问题标题】:Customising Json.NET serialisation based on compile time type根据编译时类型自定义 Json.NET 序列化
【发布时间】:2021-05-31 13:58:09
【问题描述】:

考虑以下Json.NET 序列化样式代码:

[JsonConverter(typeof(MyConverter))]
class A {
  ...
}

[JsonConverter(typeof(MyConverter))]
class B : A {
  ...
}

class MyConverter {
  public override void WriteJson(
    JsonWriter writer, 
    object value, 
    JsonSerializer serializer) { ... }
}

class CA {
  public readonly A _x;
  CA(A x) { _x = x; }
}

class CB {
  public readonly B _x;
  CB(B x) { _x = x; }
}

private static void Main(string[] args) {
  B b = new B(...);
  CA ca = new CA(b);
  CB cb = new CB(b);
 
  string caStr = JsonConvert.SerializeObject(ca);
  string cbStr = JsonConvert.SerializeObject(cb);
}

在上面的代码中,CACB 都被序列化为完全相同的字符串 caStrcbStr。但我希望MyConverter 了解编译时类型并采取不同的行动。在CB 的情况下,包含的B _x 应该只是以默认方式序列化。在CA 的情况下,MyConverter 应该标记它以某种定义的方式序列化的内容,然后以通常的默认方式序列化包含的A _x(其运行时类型为B)。 /p>

所以,我的问题是,自定义 JsonConverter 可以接收有关它正在序列化的类型的编译时类型的信息吗?在这种情况下,调整序列化调用是行不通的,因为我还需要它来处理成员对象,我不会直接调用序列化。

请注意,虽然我实际上在做的是 类似TypeNameHandling,但它并不完全相同,我需要符合外部规范,所以我真的需要这里的自定义行为。

【问题讨论】:

  • 能否请您edit 分享一个可以编译、链接和运行的minimal reproducible example,并包含您要为您的类AB 生成的JSON?在您写的 cmets 如问题中所述,我需要遵守规范,因为其他应用程序会读取数据,但您不知道该规范是什么,所以我们必须猜测。

标签: c# json.net


【解决方案1】:

您可以利用应用于属性的转换器优先于其他转换器这一事实来为A _x 使用自定义转换器实例。

正如其Serialization Guide 中所解释的:

JsonConverters 可以在许多地方定义和指定:在成员的属性中,在类的属性中,以及添加到 JsonSerializer 的转换器集合中。 使用JsonConverter的优先级是成员上的属性定义的JsonConverter,然后是类上的属性定义的JsonConverter,最后是传递给JsonSerializer的任何转换器。

这使得在使用converter parameters 应用于A _x 时,可以将声明的类型信息(或任何其他编译时信息)传递给MyConverter。在 cmets 中,您编写了 我需要向自定义转换器显示编译时类型,所以如果 编译时类型 您的意思是 声明的类型 A _x 你可以这样做。

修改MyConverter以添加带有declaredType参数的构造函数:

class MyConverter : JsonConverter 
{
    public MyConverter() : this(typeof(A)) { }

    public MyConverter(Type declaredType) => this.DeclaredType = declaredType;

    public Type DeclaredType { get; init; }

    public override void WriteJson( JsonWriter writer,  object value, JsonSerializer serializer)
    { 
        // This is just a mockup, your question doesn't specify your requirements.
        var a = (A)value;
        writer.WriteStartObject();
        if (DeclaredType == typeof(A) && a is B b)
        {
            //By in the case of CA, MyConverter should tag what it serialises in some defined way
            writer.WritePropertyName("isB");
            writer.WriteValue(true);
        }
        // Add whatever logic you need to serialize A
        // Add whatever additional logic you need to serialize B
        // And end the object
        writer.WriteEndObject();
    }

    public override bool CanConvert(Type objectType) => typeof(A).IsAssignableFrom(objectType);
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
}

然后如下修改你的类:

class CA {
    [JsonConverter(typeof(MyConverter), typeof(A))]
    public readonly A _x;
    public CA(A _x) { this._x = _x; } // The constructor argument names must have the same case-invariant name as the corresponding member for the converter to be applied
}

class CB { 
    [JsonConverter(typeof(MyConverter), typeof(B))]
    public readonly B _x;
    public CB(B _x) { this._x = _x; }
}

[JsonConverter(typeof(MyConverter))]
class A {
}

[JsonConverter(typeof(MyConverter), typeof(B))]
class B : A {
}

或者,您可以在声明的类型中创建MyConverter generic

class MyConverter<TDeclared> : JsonConverter<TDeclared> where TDeclared : A
{
    public override void WriteJson( JsonWriter writer,  TDeclared value, JsonSerializer serializer)
    { 
        // This is just a mockup, your question doesn't specify your requirements.
        writer.WriteStartObject();
        if (typeof(TDeclared) != value.GetType())
        {
            //By in the case of CA, MyConverter should tag what it serialises in some defined way
            writer.WritePropertyName("isB");
            writer.WriteValue(value.GetType() == typeof(B));
        }
        // Add whatever logic you need to serialize A
        // Add whatever additional logic you need to serialize B
        // And end the object
        writer.WriteEndObject();
    }
    public override TDeclared ReadJson(JsonReader reader, Type objectType, TDeclared existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
}

并应用如下:

class CA {
    [JsonConverter(typeof(MyConverter<A>))]
    public readonly A _x;
    public CA(A _x) { this._x = _x; } // The constructor argument names must have the same case-invariant name as the corresponding member for the converter to be applied
}

class CB { 
    [JsonConverter(typeof(MyConverter<B>))]
    public readonly B _x;
    public CB(B _x) { this._x = _x; }
}

[JsonConverter(typeof(MyConverter<A>))]
class A {
}

[JsonConverter(typeof(MyConverter<B>))]
class B : A {
}

无论哪种方式,CA ca 都会被序列化如下:

{"_x":{"isB":true}}

虽然CB cb 仍会像这样被序列化:

{"_x":{}}

注意事项:

  • 由于 Json.NET 是一个基于契约的序列化器,它在序列化对象时通常不会提供有关当前序列化堆栈的信息。父对象决定序列化什么,子对象决定如何序列化自己。因此,例如MyConverter 不会被告知容器对象的类型是 CA 还是 CB 或者正在写入该容器的特定属性。

    自定义成员转换器是该一般原则的一个显着例外。

  • 在这两种情况下,我的代码都假定您不希望在独立序列化时标记B。如果您确实希望将其标记为独立,请编辑您的问题以澄清。

  • 当使用参数化构造函数反序列化不可变类型时,Json.NET 使用大小写不变匹配将参数名称与属性匹配,以便使用查找和使用可能应用于属性的 [JsonProperty][JsonConverter] 属性。因此,我将您的构造函数参数名称修改为 _x 以确保在反序列化期间应用 MyConverter

演示小提琴#1 here 用于参数化转换器,#2 here 用于通用版本。

【讨论】:

    【解决方案2】:

    Json.NET 有一个 TypeNameHandling 配置参数来控制此行为。

    class CA 
    {
        A _x;
        CA(A x) { _x = x; }
    
        // Added this just so we can confirm the solution easily
        public A X => _x;
    }
    
    B b = new B(...);
    CA ca = new CA(b);
    
    string ca_json = JsonConvert.SerializeObject(ca, new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.All
    });
    
    var ca_deserialized = JsonConvert.DeserializeObject<CA>(ca_json);
    
    Console.WriteLine(ca_deserialized.X is B); // true
    

    激活后,Json.NET 会在 JSON 中添加一个隐藏的“$type”字段,它会准确地告诉反序列化器使用哪种类型。如果您检查 JSON,您会看到以下内容:

    "$type": "MyNamespace.CA",
    

    【讨论】:

    • 如问题中所述,我需要遵守规范,因为其他应用程序会读取数据,因此这无济于事。我需要向自定义转换器显示编译时类型
    • 所以TypeNameHandling,至少在上面的表格中,没有帮助,正如问题所提到的那样。
    • @Clinton:从语言的角度来看,您的要求还不是很清楚。您希望类型由 code 决定,而不是 JSON 数据(根据您的评论)。但是您的代码明确指出CA._x 的类型为A,但您想使用B 类型。让我们创建一个new CA(new A())new CA(new B())。序列化后,它们必须看起来相同(根据您需要符合规范的评论)。那么,当它们被反序列化时,代码究竟意味着如何能够为这两个结构相同的 JSON 字符串选择为 _x 使用不同的类型?
    • @Clinton:核心问题是:并非所有 CA 对象都有_x 类型为B。因此,在反序列化过程中,必须在使用A 类型或B 类型之间做出选择。谁在做决定,他们能够根据什么信息做出决定,在哪里可以找到/获得这些信息? (请注意,“谁”是指类、外部服务人,而不仅仅是人)
    • 当 Json.NET 通过 CACB 反射时(因为它需要对它们进行序列化),它可以知道编译时间类型。就像反序列化一样,需要使用反射来确定编译时类型,以便知道要序列化调用什么。问题是虽然ReadJson 有一个Type 类型的参数,它指示编译时间类型,但WriteJson 没有。如果是这样,我就不会有这个问题。 Json.NET 我想有这个信息(确实在TypeNameHandling 开启时它需要它,但我只是不知道如何在WriteJson 的情况下访问它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-15
    • 1970-01-01
    • 2017-06-30
    • 2014-10-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多