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"
The same in .NET 8:
[HttpPost]
public ActionResult SimplePost([FromBody] SimpleModel m )
{
return View();
}
Result in .NET 8:
m is NULL
I tried
public SimpleModel() { Name = string.Empty; Id = 1;}
public long Id { get; set; } = 1;
public string name { get; set; } = string.Empty;
[DefaultValue(0)]
[DefaultValue("")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
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());
});