如何在序列化派生类型集合时使用[JsonIgnore]



环境:.NET 6 WebAPI应用程序

我有两个类,基类和派生类,它们都可以用于将某个方法的输出序列化为JSON并将其发送到客户端。它们是这样的:

public class Base
{
public int? Prop1 { get; set; }
public string? Prop2 { get; set; }
public long? Prop3 { get; set; }
...
}
public class Derived: Base 
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public new int? Prop1 { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public new string? Prop2 { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public new long? Prop3 { get; set; }
...
}

和具有基对象集合的泛型模型类:

public class Model
{
public List<Base>? Properties { get; set; }
...
}

我想始终序列化Properties集合内的Base对象的键,但如果我序列化Derived对象的集合,则跳过值为null的键。我想要实现的示例代码:

var baseModel = new Model{ Properties = new List<Base>{ new Base { Prop1 = 1 } } };
var serialized = JsonSerializer.Serialize(baseModel);
// This returns '{ "properties": { "Prop1": 1, "Prop2": null, "Prop3": null }}'
var derivedModel = new Model { Properties = new List<Derived>{ new Derived { Prop1 = 1 }}};
// This doesn't compile because of type mismatch

var derivedModel2 = new Model { Properties = new List<Base>{ (Base)new Derived { Prop1 = 1 }}}; 
// This works, but also returns '{ "properties": { "Prop1": 1, "Prop2": null, "Prop3": null }}'
// I need to get '{ "properties": { "Prop1": 1 } }' here

有什么建议吗?

UPD:我考虑过一个泛型类的使用,但是我的模型目前以以下方式使用(简化):

public class BusinessLogic: IBusinessLogic
{
... // Constructor with DI etc.
public async Task<Model> GetStuff(...)
{
...
var model = GetModelInternal(...);
...
return model;
}
}
public interface IBusinessLogic
{
...
public Task<Model> GetStuff(...);
...
} 
public class MyController: ApiController
{
protected readonly IBusinessLogic _bl;
public MyController(..., IBusinessLogic bl)
{
_bl = bl;
}
[HttpGet]
public async Task<IActionResult> GetStuff(bool baseOrDerived, ...)
{
var model = await _bl.GetModel(baseOrDerived, ...);
return Json(model);
}
}

返回对象的类型(Base或Derived)需要依赖于我从API客户端获得的输入参数baseOrDerived。这意味着为了使用泛型,我需要在整个控制器中传递类型参数。此外,我将不得不向IBusinessLogic/BusinessLogic对引入相同的参数,而不是简单地从DI获得IBusinessLogic实例,我必须在那里获得ServiceProvider实例,在动作内部创建一个作用域并动态地构建IBusinessLogic的模板实例。考虑到这不是我想要这种行为的唯一类,这对我来说似乎真的有点小题大做。

在序列化多态类型层次结构时,System.Text.Json根据对象的声明方式为每个要序列化的对象派生一个契约而不是它实际的具体类型。因此,如果我创建Derived的实例并将其序列化为DerivedBase,我会得到不同的结果:{}用于Derived,但{"Prop1":null,"Prop2":null,"Prop3":null}用于Base:

var model = new Derived ();
Console.WriteLine("JSON when serialized as {0}", nameof(Derived));
Console.WriteLine(JsonSerializer.Serialize<Derived>(model));  // Outputs {}
Console.WriteLine("JSON when serialized as {0}", nameof(Base));
Console.WriteLine(JsonSerializer.Serialize<Base>(model));     // Outputs {"Prop1":null,"Prop2":null,"Prop3":null} 
但是请注意,有一个重要的例外:如果要序列化的值被声明为object,它将被序列化为实际的具体类型:
Console.WriteLine("JSON when serialized as {0}", nameof(System.Object));
Console.WriteLine(JsonSerializer.Serialize<object>(model));     // Outputs {} 

详细信息,请参见序列化派生类的属性。

在这里演示小提琴#1。

因此,假设您只需要序列化,您可以通过为Properties添加代理IEnumerable<object>属性来修改Model,如下所示:
public class Model
{
[JsonIgnore]
public List<Base>? Properties { get; set; }
[JsonPropertyName(nameof(Properties))] // Surrogate property for Properties with items declared as object
public IEnumerable<object>? SerializedProperties => Properties?.AsEnumerable();
}

Properties的内容将根据其实际的具体类型序列化为BaseDerived

在这里演示小提琴#2。

顺便说一句,你的Derived的定义似乎有点尴尬,因为你只是屏蔽基类的属性。您可以考虑将它们设置为虚拟,并重写它们:
public class Base
{
public virtual int? Prop1 { get; set; }
public virtual string? Prop2 { get; set; }
public virtual long? Prop3 { get; set; }
}
public class Derived: Base 
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public override int? Prop1 { get => base.Prop1; set => base.Prop1 = value; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public override string? Prop2 { get => base.Prop2; set => base.Prop2 = value; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public override long? Prop3 { get => base.Prop3; set => base.Prop3 = value; }
}

您将得到相同的序列化结果,而不需要重复的属性。

在这里演示小提琴#3。

顺便提一下,.NET 7中的您将不需要使用代理object属性,您将能够通过将[JsonDerivedType(typeof(Derived))]添加到Base来指示类型Derived的对象应该被序列化,即使声明为Base:
[JsonDerivedType(typeof(Derived))]
public class Base
{
// Remainder unchanged

。. NET 7还将通过契约自定义支持条件序列化,这可能允许你在完全不需要类型层次结构的情况下获得所需的序列化结果。

作为替代,如果您有可空属性,您有时想要序列化,有时不想,您可能需要考虑使用Optional<T>模式,如Maxime Rossini在这个问题中所示。该问题对Optional<T>的定义如下:

//  Converter from https://stackoverflow.com/questions/63418549/custom-json-serializer-for-optional-property-with-system-text-json/
//  With the fix for OptionalConverterInner<T>.Write() taken from https://stackoverflow.com/a/63431434/3744182
[JsonConverter(typeof(OptionalConverter))]
public readonly struct Optional<T>
{
public Optional(T value)
{
this.HasValue = true;
this.Value = value;
}
public bool HasValue { get; }
public T Value { get; }
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}

如果您重新定义您的Base如下:

public class Base
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<int?> Prop1 { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<string?> Prop2 { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Optional<long?> Prop3 { get; set; }
}

您可以完全消除对Derived的需求。在这个模型中,Prop1,Prop2Prop3只有在显式设置时才会序列化。,因此您将能够执行如下操作:

// Explicitly set null properties if baseOrDerived is false
var item = baseOrDerived ? new Base { Prop2 = "hello" } : new Base { Prop1 = null, Prop2 = "hello", Prop3 = null };

根据baseOrDerived是否为真,结果将是{"Prop2":"hello"}{"Prop1":null,"Prop2":"hello","Prop3":null}

在这里演示小提琴#4。

一个选择是使Model通用,像这样:

public class Model<T> where T : Base
{
public List<T>? Properties { get; set; }
...
}

现在你应该能够做:

var derivedModel = new Model<Derived> { Properties = new List<Derived>{ new Derived { Prop1 = 1 }}};

假设您正在使用System.Text.Json,您是否尝试配置序列化器选项以跳过序列化null属性?这些选项可用于其他json序列化库(如Newtonsoft)

JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

如果你想要序列化一些属性,只要用一个合理的默认值填充它。

对于[JsonIgnore]备选方案,请查看多态序列化

最新更新