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.