I was recently in an office hour with folks who had recently upgraded an ASP.NET Core MVC application to .NET 6. All UI interactions began failing with very odd behavior in the upgraded version: Request models were entering actions as null, or sometimes with all the values being null. From the onset, it was an issue with model binding, but how was it breaking?

In this post, we’ll explore why you may run into these issues and some potential solutions to your problem.

The System.Text.Json Rigidity Problem

With newer releases of ASP.NET Core, the ASP.NET team has decided to move to System.Text.Json for all JSON serializations and deserializations. Generally, this works pretty well, but you need to be aware of potential pitfalls.

Case Sensitivity

By default, System.Text.Json has a case sensitivity flag. Case sensitivity means the library may miss any variations in casing during the deserialization process. If you believe this is your issue, you can explicitly set the SerializerOptions.PropertyNameCaseInsensitive to true.

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.PropertyNameCaseInsensitive = true;
});

Trailing Commas

In my opinion, trailing commas are one of the more insidious issues developers can run into, as our brains can make them near invisible when looking at JSON. Let’s look at a failing JSON request.

{
  "really": true,
}

Note the trailing comma at the end of the property. The snippet is not valid JSON. To make System.Text.Json more forgiving, set the AllowTrailingCommas property to true.

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.AllowTrailingCommas = true;
});

Now you can accept this request with no serialization issues.

Native JSON types

System.Text.Json can be very strict about values on JSON requests, especially bool types. Let’s look at the C# type, along with two JSON requests.

record Renegade(
    bool? Really,
    DateTimeOffset? Infinity,
    int? Number,
    string? Message);

The Really property is a nullable boolean. Now, let’s see two JSON requests. One will fail to serialize while the other succeeds.

{
  "really": "true"
}

If you see any error at all, you’ll likely see something similar to these messages.

 ---> System.Text.Json.JsonException: The JSON value could not be converted to Renegade. Path: $.really | LineNumber: 1 | BytePositionInLine: 18.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'String' as a boolean.

JSON has native support for booleans, and values should be true or false without the quotes. So let’s fix the problem JSON.

{
  "really": true
}

Now, your model should be serialized correctly. Oddly, the serialization of int properties is more forgiving, supporting both the quotes and unquoted variations.

DateTime and DateTimeOffset Ambiguity

Deserializing dates can be difficult, especially when dealing with user input. So first, let’s look at a failing request.

{
  "Infinity": "01/02/2022"
}

Why would this fail? The date is clearly January 2nd, 2022, or is it February 1st, 2022? Rather than parse your ambiguous date, System.Text.Json will not try. You have two solutions here:

Fix the formatting to align closer to the ISO format. i.e 2022-01-02. Create a new JsonConverter to parse dates based on the format you perceive the date to be.

Let’s look at the JsonConverter.

public class NullableDateTimeConverter : JsonConverter<DateTime?>
{
    private const string Format = "MM/dd/yyyy";
    
    public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        reader.GetString() is { } value 
        && TryParseExact(value, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) 
            ? result : null;

    public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
    {
        if (value is { }) {
            writer.WriteStringValue(value.Value.ToString(Format, CultureInfo.InvariantCulture));
        }
        else {
            writer.WriteNullValue();
        }
    }
}

You can implement this converter for each variation in the date family to be more explicit about date formats. You need to add your converters to the Converters collection at startup.

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.Converters.Add(new NullableDateTimeConverter());
});

Conclusion

So if you’ve recently upgraded to using System.Text.Json and having issues, you can help narrow down your problem by first looking at your model and JSON requests to see if you’ve made any of these mistakes. There are several ways to solve the issues, by either changing the values on JSON requests or adding converters. It’s up to you which you choose and how you solve your problems.

Thanks for reading, and I hope this post has helped.