18

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.

Igor
  • 349
  • 1
  • 6
Dabbas
  • 3,112
  • 7
  • 42
  • 75
  • 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. – Dabbas Nov 26 '20 at 14:09
  • Yes I'm using Nullable types, I just clarified that in the question – Dabbas Nov 26 '20 at 14:31
  • 6
    Not sure why this was closed, it's perfectly clear and contains a [mcve]. – dbc Nov 27 '20 at 01:55

1 Answers1

20

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • `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? – zigzag May 11 '21 at 18:05
  • @zigzag - are you setting [`JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString`](https://learn.microsoft.com/en-us/dotnet/core/compatibility/serialization/5.0/jsonserializer-allows-reading-numbers-as-strings) which was introduced in .NET 5? – dbc May 11 '21 at 18:09
  • 1
    Yes, but `valueConverter.Read(...)` call skips that logic. See [source](https://github.com/dotnet/runtime/blob/49f4fb85df7339eddd07ce4b6065d370cd93d085/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs#L13). – zigzag May 11 '21 at 18:21
  • 1
    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` and just call `Serialize` and `Deserialize` from `Read()` and `Write()` and I think you should be all set. – dbc May 11 '21 at 18:24
  • 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.Json – viggity May 02 '22 at 16:14
  • There' s already a `ReadOnlySpan.Empty` so you don't need your "Empty" variable. Otherwise good answer, helped me write my own converter for `int?`, upvoted – Alex from Jitbit Jul 12 '23 at 23:35