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.
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.
Text. Json. Serialization namespace, which contains attributes and APIs for advanced scenarios and customization specific to serialization and deserialization.
Json.net is made by newtonsoft.
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.
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:
(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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With