Solving .NET JSON Deserialization Issues
/ 5 min read
Updated:Found this post helpful?
Buy me a coffeeTable of Contents
What is more frustrating than a sem-working solution, am I right? Unfortunately, when it comes to JSON serialization, that can mean getting some of the data sometimes. You’re left scratching your head and asking, “Why is my data not coming through correctly?”
Your JSON is deserializing as null because .NET’s default JsonSerializer expects PascalCase property names, while most JSON APIs return camelCase. Fix it by using JsonSerializerDefaults.Web or setting PropertyNameCaseInsensitive = true.
In this post, we’ll explore the different JsonSerializerOptions you might run into with .NET and how you can predict the behavior of your serialization. Let’s get started.
Why Is My JSON Deserializing as Null?
When working with JSON, two parties are typically involved: The producer and the consumer. Unfortunately, more often than not, when running into JSON deserialization issues, we often find that we misunderstood the producer’s JSON format and likely misconfigured something on our end. Let’s look at an example response from a hypothetical JSON API.
[ { "name": "Khalid Abuhakmeh", "hobby" : "Video Games" }, { "name": "Maarten Balliauw", "hobby" : "Gardening" }]There are a few characteristics to note about this response.
- The top-level element is an array.
- There are multiple elements in the array. Two, to be exact.
- All fields are quoted.
- All field names are camel-cased.
To process this JSON response, we need to set up our serialization options to match the JSON serialization choices of our producer. In the next section, let’s look at ways to fail and succeed at deserialization.
camelCase vs PascalCase in System.Text.Json
The first likely step folks will take to use the JsonSerializer class as is, with a straight call to Deserialize<T>.
var @default = JsonSerializer.Deserialize<Person[]>(json);Console.WriteLine($"default: {@default?[0]}");I’m sad to say this is incorrect, as the default serializer options work from “PascalCase”. So the result of this deserialization will produce entities, but their values will be null or empty.
default: Person { Name = , Hobby = }Oops! Where are the Name and the Hobby? Well, it turns out that we have a naming mismatch. So how do we fix this issue? There are a few options.
| Option | Behavior | When to Use |
|---|---|---|
Default JsonSerializer.Deserialize<T>() |
Expects exact PascalCase property names | Only when the producer also emits PascalCase |
JsonSerializerDefaults.Web |
Matches camelCase property names | Calling typical camelCase JSON APIs (most common case) |
PropertyNameCaseInsensitive = true |
Ignores case entirely when matching names | Producer casing is inconsistent or unknown |
How to Deserialize Case-Insensitive JSON
The first way to fix this issue is to use a different set of options for the JsonSerializer. For example, we can use the JsonSerializerDefaults class to choose the Web option.
// camelCasevar web = JsonSerializer.Deserialize<Person[]>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));Console.WriteLine($"web: {web?[0]}");Choosing the JsonSerializerDefaults.Web value for JsonSerializerOptions defaults to the camel case for all JSON field names. Running the code above, we have corrected our initial issues.
web: Person { Name = Khalid Abuhakmeh, Hobby = Video Games }What if we have an unreliable producer, and they may inadvertently change their mind about casing? Well, there’s one more option.
// any Casevar any = JsonSerializer.Deserialize<Person[]>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });Console.WriteLine($"case insensitive: {any?[0]}");This option will ignore casing on field names and map accordingly but be warned. Ignoring the case sensitivity of names will cause issues if the producer uses the same name for different fields. Running the code sample, we get the following result.
case insensitive: Person { Name = Khalid Abuhakmeh, Hobby = Video Games }Cool! The solution still works, but there’s still one more solution to look over.
You’ll likely use the HttpClient class when working with JSON APIs. The HttpClient uses the JsonSerializerDefaults.Web options internally, so you don’t need any additional changes if you’re calling a camel-cased API.
// stubbing the web request to return the JSONvar httpClient = new HttpClient(new StubHandler(json));
// uses JsonSerializerOptions(JsonSerializerDefaults.Web) by defaultvar response = await httpClient.GetFromJsonAsync<Person[]>("https://example.com");Console.WriteLine($"http client: {response?[0]}");Running this code, we see the results we expect.
http client: Person { Name = Khalid Abuhakmeh, Hobby = Video Games }You can also pass in any options to override the default behavior of the GetFromJsonAsync method.
var otherResponse = await httpClient.GetFromJsonAsync<Person[]>( "https://example.com", new JsonSerializerOptions { PropertyNameCaseInsensitive = true });Console.WriteLine($"http client w/ options: {otherResponse?[0]}");Running this sample, we still see the correct results.
http client w/ options: Person { Name = Khalid Abuhakmeh, Hobby = Video Games }So, there we have it—a run-down of solving your serialization/deserialization issues when working with existing JSON APIs. I’ve included the complete solution below if you’d like to work with this sample.
FAQ
Why does JsonSerializer.Deserialize<T>() return nulls instead of throwing an error?
By default, unmatched JSON properties are silently ignored rather than causing an exception, which is why mismatched casing produces empty objects instead of obvious errors.
Does HttpClient.GetFromJsonAsync use camelCase by default?
Yes. HttpClient’s JSON extension methods use JsonSerializerDefaults.Web internally, so camelCase APIs work out of the box without extra configuration.
Is PropertyNameCaseInsensitive slower than exact matching?
There’s a small performance cost since the matching can’t rely on exact string comparisons, but for most applications it’s negligible compared to the reliability gained.
If you’re dealing with more advanced scenarios, check out polymorphic serialization with System.Text.Json, serializing interface instances with System.Text.Json, or speeding up ASP.NET Core JSON APIs with source generators.
As always, thanks for reading and sharing my posts.
using System.Net;using System.Net.Http.Json;using System.Text.Json;
// language=jsonvar json = """[ { "name": "Khalid Abuhakmeh", "hobby" : "Video Games" }, { "name": "Maarten Balliauw", "hobby" : "Gardening" }]""";
// PascalCasevar @default = JsonSerializer.Deserialize<Person[]>(json);Console.WriteLine($"default: {@default?[0]}");
// CamelCasevar web = JsonSerializer.Deserialize<Person[]>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));Console.WriteLine($"web: {web?[0]}");
// any Casevar any = JsonSerializer.Deserialize<Person[]>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });Console.WriteLine($"case insensitive: {any?[0]}");
// stubbing the web request to return the JSONvar httpClient = new HttpClient(new StubHandler(json));
// uses JsonSerializerOptions(JsonSerializerDefaults.Web) by defaultvar response = await httpClient.GetFromJsonAsync<Person[]>("https://example.com");Console.WriteLine($"http client: {response?[0]}");
var otherResponse = await httpClient.GetFromJsonAsync<Person[]>( "https://example.com", new JsonSerializerOptions { PropertyNameCaseInsensitive = true });Console.WriteLine($"http client w/ options: {otherResponse?[0]}");
// ReSharper disable once ClassNeverInstantiated.Globalpublic record Person(string Name, string Hobby);
public class StubHandler : DelegatingHandler{ private readonly string _response;
public StubHandler(string response) { _response = response; }
protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(_response), RequestMessage = request }); }}