Home > database >  How to use [JsonIgnore] when serializing a collection of derived types
How to use [JsonIgnore] when serializing a collection of derived types

Time:11-17

Environment: .NET 6 WebAPI app

I have two classes, base an derived, that both can be used to serialize the output of a certain method as JSON and send it to client. They look like this:

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; }
  ...
}

and a generic model class that has a collection of Base objects:

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

I would like to always serialize the keys of Base objects inside the Properties collection, but skip the keys where the values are null if I'm serializing a collection of Derived objects. Sample code of what I want to achieve:

  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

Any advice on where to look?

UPD: I've considered a generic class use, but my model is currently used in the following manner (simplified):

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);
  }
}

The type of the return objects (Base or Derived) needs to depend upon the input parameter baseOrDerived that I get from the API client. This means that in order to use a generic I would need to pass the type parameter all the way through the controller. Moreover, I will have to introduce the same parameter to the IBusinessLogic/BusinessLogic pair and instead of simply getting IBusinessLogic instance from the DI, I would have to get a ServiceProvider instance there, create a scope inside the action and construct templated instance of IBusinessLogic dynamically. Given that this is NOT the only class I want this behavior from, this seems a real overkill to me.

CodePudding user response:

One option is to make Model generic, like this:

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

Now you should be able to do:

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

CodePudding user response:

Assuming you are using System.Text.Json, have you tried configuring the serializer options to skip serializing null properties ? These options are available for other json serialization libraries (like Newtonsoft)

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

In case you want some property serialized anyway, just populate it with a reazonable default value.

For [JsonIgnore] alternative, have a look at polymorphic serialization

CodePudding user response:

When serializing a polymorphic type hierarchy, System.Text.Json derives a contract for each object to be serialized from how the object is declared rather than its actual, concrete type. Thus if I create an instance of Derived and serialize it as both Derived and Base and, I get different results: {} for Derived but {"Prop1":null,"Prop2":null,"Prop3":null} for 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} 

Note, however, that there is one important exception: if the value to be serialized is declared as object, it will be serialized as its actual, concrete type:

Console.WriteLine("JSON when serialized as {0}", nameof(System.Object));
Console.WriteLine(JsonSerializer.Serialize<object>(model));     // Outputs {} 

For details, see Serialize properties of derived classes.

Demo fiddle #1 here.

Thus, assuming you only need to serialize, you may modify your Model by adding a surrogate IEnumerable<object> property for Properties as follows:

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();
}

And the contents of Properties will be serialized as Base or Derived depending upon their actual, concrete type.

Demo fiddle #2 here.

Incidentally, your definition of Derived seems a little awkward because you are simply masking the properties of the base class. You might consider making them virtual, and overriding them instead:

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; }
}

You will get the same serialization results without the duplication of properties.

Demo fiddle #3 here.

Incidentally, in .NET 7 you will will not need to use a surrogate object property, you will be able to indicate that objects of type Derived should be serialized a such even when declared as Base by adding [JsonDerivedType(typeof(Derived))] to Base:

[JsonDerivedType(typeof(Derived))]
public class Base
{
    // Remainder unchanged

.NET 7 will also support Conditional serialization via contract customization which might allow you to achieve your required serialization results without needing a type hierarchy at all.

As an alternative, if you have nullable properties that you sometimes want to serialize, and sometimes don't, you might want to consider using the Optional<T> pattern as shown in this question by Maxime Rossini. That question defines Optional<T> as follows:

//  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";
}

If you redefine your Base as follows:

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; }
}

You can completely eliminate the need for Derived. With this model, Prop1, Prop2 and Prop3 will only be serialized when set explicitly, so you will be able to do something like:

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

And the result will be {"Prop2":"hello"} or {"Prop1":null,"Prop2":"hello","Prop3":null} depending upon whether baseOrDerived is true.

Demo fiddle #4 here.

  • Related