I have seen this exact symptom show up on more than one team migrating an ASP.NET Core MVC app to .NET 6. Everything builds fine, the app starts, but request models arrive in the action as null, or every property inside them is null even though the client is clearly sending real data. The instinct is to suspect model binding is broken. In most cases the real cause sits one layer below that, in how System.Text.Json parses the incoming JSON in the first place.
Since .NET Core 3, the framework has moved away from Newtonsoft.Json towards System.Text.Json as the default serializer, and System.Text.Json is deliberately stricter about what it accepts. That strictness is a reasonable design choice for performance and security, but it means JSON that Newtonsoft happily tolerated for years can now fail silently or throw, and the failure often looks like a model binding bug rather than a serialization one.
Case Sensitivity Is On by Default
System.Text.Json matches property names by exact case by default. If the client sends camelCase keys and your C# properties are PascalCase, this is usually handled by the default naming policy, but any mismatch outside that convention will silently fail to bind rather than throw an obvious error, which makes it easy to miss during testing with well-behaved clients and only surface with a third-party integration sending slightly different casing.
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNameCaseInsensitive = true;
});
Setting PropertyNameCaseInsensitive to true is a reasonable default to reach for defensively, especially in APIs consumed by clients you do not fully control. It costs very little at runtime and removes an entire category of silent binding failures.
Trailing Commas Break the Whole Payload
This is one of the more frustrating issues because a trailing comma is nearly invisible to the human eye when scanning a JSON payload, especially one that was hand-edited or generated by a template with a loop.
{
"really": true,
}
That trailing comma after true makes the whole document invalid JSON under strict parsing rules, and by default System.Text.Json rejects it outright rather than silently ignoring it. If you are dealing with clients or tools that occasionally produce trailing commas, and you cannot fix the sender, allow it explicitly on the receiving end.
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.AllowTrailingCommas = true;
});
Native JSON Types Are Enforced Strictly
System.Text.Json is strict about matching JSON’s actual type system, and booleans are where this bites people the most, since a lot of client code and query string handling ends up sending boolean-looking values as quoted strings out of habit.
record Renegade(
bool? Really,
DateTimeOffset? Infinity,
int? Number,
string? Message);
A request sending the boolean as a quoted string fails to deserialize entirely.
{
"really": "true"
}
and the resulting exception points at the property but not always obviously at the real cause.
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.
The fix here is on the sending side, not the receiving side, send an actual JSON boolean rather than a quoted string.
{
"really": true
}
Interestingly, integer properties are more forgiving in practice, System.Text.Json accepts both quoted and unquoted numeric values for int properties even though the same strictness applies conceptually. Do not assume that leniency extends to booleans just because it holds for numbers, the two are handled differently under the hood.
DateTime Parsing Refuses to Guess
Date parsing is where System.Text.Json’s strictness is arguably a feature rather than an annoyance, even though it does not feel that way the first time you hit it.
{
"Infinity": "01/02/2022"
}
Is that January 2nd or February 1st? There is no way to know without knowing the sender’s locale convention, and System.Text.Json refuses to guess rather than silently picking one interpretation and risking a wrong date slipping through unnoticed. You have two real options here. Either fix the format at the source to something unambiguous like ISO 8601, 2022-01-02, or write a custom converter that parses the specific ambiguous format you actually expect to receive.
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
&& DateTime.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();
}
}
}
Register the converter through the same JsonOptions configuration used for the other settings above.
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new NullableDateTimeConverter());
});
If your app genuinely needs to accept several different date formats from different clients, write a separate converter per format rather than trying to cram multiple parsing attempts into one converter’s Read method. It keeps each converter’s failure mode predictable, and you can register only the ones a given endpoint actually needs.
How to Approach an Upgrade Without Guessing
When request models start coming through as null after a .NET 6 upgrade, the fastest path to a diagnosis is comparing the actual JSON payload against your model’s property types field by field, rather than adding logging around the model binder itself. In my experience the culprit is almost always one of the four issues above, casing, trailing commas, a quoted primitive that should be unquoted, or an ambiguous date format, and checking the raw payload first saves a lot of time compared to stepping through binder internals.
It is worth deciding early in a migration whether you want to loosen System.Text.Json’s defaults broadly with settings like case insensitivity and trailing comma tolerance, or fix the actual payloads and clients sending non-conformant JSON. Loosening the parser is faster to ship, but it also means your API quietly tolerates malformed input indefinitely. For an internal API where you control both ends, fixing the client is usually the better long-term investment. For a public API with clients you do not control, some defensive loosening on the receiving end is often the pragmatic choice.
Leave a Reply