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?”

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.

Starting With Plain-old JSON

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.

  1. The top-level element is an array.
  2. There are multiple elements in the array. Two, to be exact.
  3. All fields are quoted.
  4. 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.

Attempting to Deserialize 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.

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.

// camelCase
var 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 Case
var 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 JSON
var httpClient = new HttpClient(new StubHandler(json));

// uses JsonSerializerOptions(JsonSerializerDefaults.Web) by default
var 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.

As always, thanks for reading and sharing my posts.


using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

// language=json
var json = """
[
   {
      "name": "Khalid Abuhakmeh",
      "hobby" : "Video Games"
   },
   {
      "name": "Maarten Balliauw",
      "hobby" : "Gardening" 
   }
]
""";

// PascalCase
var @default = JsonSerializer.Deserialize<Person[]>(json);
Console.WriteLine($"default: {@default?[0]}");

// CamelCase
var web = JsonSerializer.Deserialize<Person[]>(json,
    new JsonSerializerOptions(JsonSerializerDefaults.Web));
Console.WriteLine($"web: {web?[0]}");

// any Case
var any = JsonSerializer.Deserialize<Person[]>(json,
    new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true
    });
Console.WriteLine($"case insensitive: {any?[0]}");

// stubbing the web request to return the JSON
var httpClient = new HttpClient(new StubHandler(json));

// uses JsonSerializerOptions(JsonSerializerDefaults.Web) by default
var 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.Global
public 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
        });
    }
}