Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize an empty string to a null value for all `Nullable<T>` value types using System.Text.Json? [closed]

In .Net Core 3.1 and using System.Text.Json library, I'm facing an issue that didn't occur in Newtonsoft library.

If I send an empty string in JSON for some properties of type (type in backend) DateTime? or int?, it returns 400 status code with an error message that value can't be deserialized. However, with Newtonsoft an empty string is automatically interpreted as a null value for any Nullable<T>.

A minimal example would be:

var json = "\"\"";

Assert.AreEqual(null, Newtonsoft.Json.JsonConvert.DeserializeObject<DateTime?>(json)); // Passes
Assert.AreEqual(null, System.Text.Json.JsonSerializer.Deserialize<DateTime?>(json));   // Throws System.Text.Json.JsonException: The JSON value could not be converted to System.Nullable`1[System.DateTime].

Is there any way to make System.Text.Json behave in the same way? Demo here.

like image 408
Dabbas Avatar asked Nov 26 '20 13:11

Dabbas


People also ask

What is the difference between system text JSON and Newtonsoft JSON?

System. Text. Json focuses primarily on performance, security, and standards compliance. It has some key differences in default behavior and doesn't aim to have feature parity with Newtonsoft.

What is System Text JSON?

Text. Json. Serialization namespace, which contains attributes and APIs for advanced scenarios and customization specific to serialization and deserialization.

Is JSON net the same as Newtonsoft JSON?

Json.net is made by newtonsoft.

Is Newtonsoft JSON obsolete?

Yet Newtonsoft. Json was basically scrapped by Microsoft with the coming of . NET Core 3.0 in favor of its newer offering designed for better performance, System. Text.


1 Answers

You can use the factory converter pattern to create a JsonConverterFactory that causes an empty string to be interpreted as null for all Nullable<T> type values.

The following factory does the job:

public class NullableConverterFactory : JsonConverterFactory
{
    static readonly byte [] Empty = Array.Empty<byte>();

    public override bool CanConvert(Type typeToConvert) => Nullable.GetUnderlyingType(typeToConvert) != null;

    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => 
        (JsonConverter)Activator.CreateInstance(
            typeof(NullableConverter<>).MakeGenericType(
                new Type[] { Nullable.GetUnderlyingType(type) }),
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: new object[] { options },
            culture: null);

    class NullableConverter<T> : JsonConverter<T?> where T : struct
    {
        // DO NOT CACHE the return of (JsonConverter<T>)options.GetConverter(typeof(T)) as DoubleConverter.Read() and DoubleConverter.Write()
        // DO NOT WORK for nondefault values of JsonSerializerOptions.NumberHandling which was introduced in .NET 5
        public NullableConverter(JsonSerializerOptions options) {} 

        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                if (reader.ValueTextEquals(Empty))
                    return null;
            }
            return JsonSerializer.Deserialize<T>(ref reader, options);
        }           

        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, value.Value, options);
    }
}

The factory should be added to the JsonSerializerOptions.Converters collection of your framework.

Notes:

  • In my original version of this answer I cached the return of (JsonConverter<T>)options.GetConverter(typeof(T)) for performance as recommended by Microsoft. Unfortunately as noted in comments by zigzag Microsoft's own DoubleConverter.Read() and DoubleConverter.Write() methods do not account for non-default values of JsonSerializerOptions.NumberHandling, so I have removed this logic as of .NET 5.

Demo fiddle here.

like image 107
dbc Avatar answered Sep 22 '22 06:09

dbc