alberto_dev
alberto_dev

Reputation: 33

Custom serialization of null values in Asp.net core WebApi

I have to change the default json serialization/deserialization of an object following these rules:

  1. When the C# object is null , it has to be serialize to json object with Id equals 0.
  2. When the json object has id equals 0 , it has to be deserialize to C# object with null value.

I try this:

public class EntityConverter : JsonConverter<EventDefinition>
{
    public override EventDefinition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        EventDefinition result = JsonSerializer.Deserialize<EventDefinition>(ref reader, options);
        if (result.EventDefinitionId == 0)
            return null;
        else return result;
    }

    public override void Write(Utf8JsonWriter writer, EventDefinition value, JsonSerializerOptions options)
    {
        if (value == null)
        {
            value = new EventDefinition();
            value.EventDefinitionId = 0;
            writer.WriteStringValue(JsonSerializer.Serialize(value));
        }
        else
            writer.WriteStringValue(JsonSerializer.Serialize(value));
    }
}

I need to replace writer.WriteStringValue beacuse it writes the whole object as a string and what I need is to continue with the normal serialization of the object after the modification. How can I achieved this?

Upvotes: 3

Views: 2792

Answers (1)

dbc
dbc

Reputation: 116980

.NET 5 allows custom converters to handle null if they choose. From How to write custom converters for JSON serialization (marshalling) in .NET: Handle null values:

To enable a custom converter to handle null for a reference or value type, override JsonConverter<T>.HandleNull to return true.

Thus in EntityConverter you need to add

public override bool HandleNull => true;

However, when you do you will encounter a second problem, namely that, in Write(), you are writing the serialized JSON for EventDefinition as a double-serialized string value, rather than as an object. How can it be serialized as an object (with null replaced) instead? If you have applied EntityConverter as either:

  • A [JsonConverter] applied to a property.
  • A converter added to the Converters collection.

Then you can enhance the answer from How to use default serialization in a custom System.Text.Json JsonConverter? to include HandleNull as follows:

public class EntityConverter : DefaultConverterFactory<EventDefinition>
{
    protected override bool HandleNull => true;
    
    protected override EventDefinition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions, JsonConverter<EventDefinition> defaultConverter)
    {
        var result = base.Read(ref reader, typeToConvert, modifiedOptions, defaultConverter);
        return result?.EventDefinitionId == 0 ? null : result;
    }
    
    protected override void Write(Utf8JsonWriter writer, EventDefinition value, JsonSerializerOptions modifiedOptions, JsonConverter<EventDefinition> defaultConverter) 
    {
        value ??= new EventDefinition { EventDefinitionId = 0 };
        base.Write(writer, value, modifiedOptions, defaultConverter);
    }
}

public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
    class NullHandlingDefaultConverter : DefaultConverter
    {
        public NullHandlingDefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory) : base(options, factory) { }
        public override bool HandleNull => true;
    }
    
    class DefaultConverter : JsonConverter<T>
    {
        readonly JsonSerializerOptions modifiedOptions;
        readonly DefaultConverterFactory<T> factory;
        readonly JsonConverter<T> defaultConverter;

        public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
        {
            this.factory = factory ?? throw new ArgumentNullException();
            this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
            this.defaultConverter = (JsonConverter<T>)modifiedOptions.GetConverter(typeof(T));
        }

        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions, defaultConverter);

        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions, defaultConverter);
    }

    protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter)
        => defaultConverter.ReadOrSerialize<T>(ref reader, typeToConvert, modifiedOptions);

    protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter) 
        => defaultConverter.WriteOrSerialize(writer, value, modifiedOptions);

    protected virtual bool HandleNull => false;

    public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => HandleNull ? new NullHandlingDefaultConverter(options, this) : new DefaultConverter(options, this);
}

public static class JsonSerializerExtensions
{
    public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
    {
        var copy = new JsonSerializerOptions(options);
        for (var i = copy.Converters.Count - 1; i >= 0; i--)
            if (copy.Converters[i].GetType() == converterType)
                copy.Converters.RemoveAt(i);
        return copy;
    }

    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, options);
    }

    public static T ReadOrSerialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (converter != null)
            return converter.Read(ref reader, typeToConvert, options);
        else
            return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);
    }
}

And serialize a List<EventDefinition> events as follow:

var options = new JsonSerializerOptions
{
    Converters = { new EntityConverter() },
    WriteIndented = true, // If you want
};

var json = JsonSerializer.Serialize(events, options);

Notes:

  • In .NET Core 3.x HandleNull is not available; JsonConverter<T>.Write() will never be passed a null value in that version. Thus, in that version you would need to adopt a different approach, such as adding a custom converter for the containing type(s) or serializing DTOs instead of "real" objects.

    Json.NET also will never call call its JsonConverter.WriteJson() for a null value so the the two serializers are consistent in that limitation in 3.x, see How to force JsonConverter.WriteJson() to be called for a null value for confirmation. The answer to that question shows a workaround using a custom contract resolver, so reverting to Json.NET might be an option for you in 3.1. System.Text.Json in contrast does not make its contract model public.

  • If you have applied EntityConverter directly to EventDefinition using JsonConverterAttribute, i.e.:

    [JsonConverter(typeof(EntityConverter))]
    public class EventDefinition
    {
        public int EventDefinitionId { get; set; }
    }
    

    Then the above approach will not work. In fact there does not appear to be a way to generate a "normal" serialization for an instance of a type to which a converter is applied directly. Instead you would need to manually write and read each required property inside Write() and Read(), or write your own reflection code to do so automatically.

Demo fiddle here.

Upvotes: 1

Related Questions