asp.net-core-mvc.net-8.0asp.net-core-8

Configure .Net 8 to parse json like .Net Framework 4.8 did?


While porting an application to .NET 8 the Json parser is so much more picky about the Json. How do I tell it to not be so picky? I don't have control over the front end doing the posting.

The post request:

POST http://localhost/SimplePost HTTP/1.1
Host: localhost
Content-Type: application/json

{ "Name": "WTF", "Id": null }

The model:

public class SimpleModel
{
     public string Name { get; set; }
     public long Id { get; set; }
}

.NET Framework 4.8

[HttpPost]
public ActionResult SimplePost(SimpleModel m)
{
    return View();
}

Result:

Id = 0
Name = "WTF"

enter image description here

The same in .NET 8:

[HttpPost]
public ActionResult SimplePost([FromBody] SimpleModel m )
{
    return View();
}

Result in .NET 8:

m is NULL

enter image description here

I tried


Solution

  • You can write your own JsonConverter, which needed implementation can be found here: how to set default value for object property with default value when Json property is null?

    Here is said implementation by @dbc - of course you can augment it to handle only long in only specific way, below solution is general, which handles more such conversion more broadly:

    public class NullToDefaultConverter : DefaultConverterFactory
    {
        public override bool CanConvert(Type typeToConvert) => typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) == null;
        protected sealed override bool HandleNull { get; } = true;
        protected override T? Read<T>(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions) where T:default => 
            reader.TokenType switch
            {
                JsonTokenType.Null => default(T),
                _ => base.Read<T>(ref reader, typeToConvert, modifiedOptions),
            };
    }
    
    public abstract class DefaultConverterFactory : JsonConverterFactory
    {
        // Adapted from this answer https://stackoverflow.com/a/65430421/3744182
        // To https://stackoverflow.com/questions/65430420/how-to-use-default-serialization-in-a-custom-system-text-json-jsonconverter
        class DefaultConverter<T> : JsonConverter<T> 
        {
            readonly JsonSerializerOptions modifiedOptions;
            readonly DefaultConverterFactory factory;
    
            public DefaultConverter(JsonSerializerOptions modifiedOptions, DefaultConverterFactory factory) => (this.modifiedOptions, this.factory) = (modifiedOptions, factory);
    
            public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);
            public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read<T>(ref reader, typeToConvert, modifiedOptions);
            public override bool CanConvert(Type typeToConvert) => typeof(T).IsAssignableFrom(typeToConvert);
        }
        class NullHandlingDefaultConverter<T> : DefaultConverter<T>
        {
            public NullHandlingDefaultConverter(JsonSerializerOptions modifiedOptions, DefaultConverterFactory factory) : base(modifiedOptions, factory) { }
            public override bool HandleNull => true;
        }
    
        protected virtual JsonSerializerOptions ModifyOptions(JsonSerializerOptions options) => 
            options.CopyAndRemoveConverter(this.GetType());
    
        protected virtual T? Read<T>(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions) => 
            (T?)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
    
        protected virtual void Write<T>(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions) => 
            JsonSerializer.Serialize(writer, value, modifiedOptions);
    
        public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            var converterType = (HandleNull ? typeof(NullHandlingDefaultConverter<>) : typeof(DefaultConverter<>)).MakeGenericType(typeToConvert);
            return (JsonConverter)Activator.CreateInstance(converterType, new object [] { ModifyOptions(options), this })!;
        }
        
        protected virtual bool HandleNull { get; } = false;
    }
    
    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;
        }
    }
    

    Then in your startup class (Program or Startup) you register the converter:

    // For controllers.
    builder.Services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Insert(
            0,
            new NullToDefaultConverter());
    });
    
    // For other, such as Minimal API.
    builder.Services.ConfigureHttpJsonOptions(options =>
    {
        options.SerializerOptions.Converters.Insert(
            0,
            new NullToDefaultConverter());
    });