How to deserialize an empty string to a null value for all `Nullable<T>` value types using System.Text.Json?
Asked Answered
S

1

20

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.

Spank answered 26/11, 2020 at 13:16 Comment(3)
For now if I'm facing the issue with the DateTime type, but I read someone faced the same issue for Int type, and maybe for other types, but I don't want to wait till I face the same issues for all types and create custom converter for each one.Spank
Yes I'm using Nullable types, I just clarified that in the questionSpank
Not sure why this was closed, it's perfectly clear and contains a minimal reproducible example.Proceed
P
24

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:

Demo fiddle here.

Proceed answered 26/11, 2020 at 15:43 Comment(6)
valueConverter.Read(...); doesn't handle number inside a string. Unfortunately, ReadNumberWithCustomHandling method that does handle number inside a string is internal. Any ideas how to solve this?Razorbill
@Razorbill - are you setting JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString which was introduced in .NET 5?Proceed
Yes, but valueConverter.Read(...) call skips that logic. See source.Razorbill
Well that's unfortunate. Caching the valueConverter is recommended by Microsoft purely for performance reasons, so if it the wrong converter is returned then it would be best to just eliminate it completely. Remove the valueConverter property from NullableConverter<T> and just call Serialize and Deserialize from Read() and Write() and I think you should be all set.Proceed
I hate System.Text.Json with all of my worldly being. Unless you're running on thousands of servers and NEED the performance boost, do yourself a favor and use the sane, developer friendly Newtsonsoft.JsonDeathblow
There' s already a ReadOnlySpan<byte>.Empty so you don't need your "Empty" variable. Otherwise good answer, helped me write my own converter for int?, upvotedGertrude

© 2022 - 2024 — McMap. All rights reserved.