Updated on

System.Text.Json writes JSON keys exactly as the C# properties are declared, so FirstName becomes "FirstName". To get "firstName" instead, set PropertyNamingPolicy to JsonNamingPolicy.CamelCase on a JsonSerializerOptions instance and pass it to JsonSerializer.Serialize().

ASP.NET Core does this for us already. Its endpoints, and the HttpClient JSON extensions, serialize through JsonSerializerDefaults.Web, a preset whose naming policy is camelCase, which is why the same class produces "FirstName" in a console application and "firstName" from a Web API.

Three ways to reach camelCase deliberately follow: a [JsonPropertyName] attribute per property, a JsonSerializerOptions argument per call, and the web preset that covers a whole application. The last section removes the repetition the middle one creates.

To download the source code for this article, you can visit our GitHub repository.

Let’s take a look.

What Is the Default Property Naming Policy in System.Text.Json?

System.Text.Json ships with no naming policy at all. JsonSerializerOptions.PropertyNamingPolicy is null unless we set it, so JsonSerializer writes every JSON key exactly as the C# property is declared.

Because C# convention is PascalCase, a Person with FirstName and IsActive serializes to {"FirstName":"John","IsActive":true}. The serializer is not choosing PascalCase. It is copying the member name character for character, which is why a deliberately lower-cased property keeps its lower case.

Deserialization matches the same way, and it is case-sensitive by default. A payload carrying firstName will not fill a FirstName property unless something changes the matching rules.

Two settings change them, and they are not the same setting. PropertyNamingPolicy decides how names are written on the way out and matched on the way in. PropertyNameCaseInsensitive relaxes matching on the way in only, and does nothing to the output.

Everything that follows moves that default in one of three ways: an attribute on each property, an options object at each call site, or a preset covering a whole application.

Let’s define a Person class:

public class Person
{
    public string? FirstName { get; set; }
    public string? Surname { get; set; }
    public int? Age { get; set; }
    public bool? IsActive { get; set; }
}

The reason why we are using the nullable type for Age and IsActive is when we deserialize JSON string to a Person object, if Age or IsActive are not provided, they will be deserialized to the default value (zero and false). But if we define them with the nullable type, they will be deserialized to null, which is what we want here.

If we try to serialize an instance of Person:

var person = new Person()
{
    Age = 20,
    FirstName = "John",
    Surname = "Doe",
    IsActive = true
};
Console.WriteLine($"{JsonSerializer.Serialize(person)}");

When we run the application, we will get the result:

{"FirstName":"John","Surname":"Doe","Age":20,"IsActive":true}

By default, the object can only be serialized or deserialized with the same property names in the JSON string.

But, what if we want to use JSON keys in the camel case in serialization? We have several ways to achieve that.

How Do We Serialize to camelCase With the JsonPropertyName Attribute?

One way to implement this is to add an [JsonPropertyName] attribute on properties with specific names. With this approach, we can define another class:

public class PersonWithAttributes
{
    [JsonPropertyName("firstName")]
    public string? FirstName { get; set; }

    [JsonPropertyName("surname")]
    public string? Surname { get; set; }

    [JsonPropertyName("age")]
    public int? Age { get; set; }

    [JsonPropertyName("isActive")]
    public bool? IsActive { get; set; }
}

Next, we can serialize it:

var personWithAttributes = new PersonWithAttributes
{
    Age = 20,
    FirstName = "John",
    Surname = "Doe",
    IsActive = true
};
Console.WriteLine($"{JsonSerializer.Serialize(personWithAttributes)}");

And, we can inspect the result:

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

{"firstName":"John","surname":"Doe","age":20,"isActive":true}

Notice the different naming convention from the default one.

If we have a JSON string in camel case, after we deserialize it, we can parse it correctly:

var personString = """{"firstName":"John","surname":"Doe","age":20,"isActive":true}""";
var personFromString = JsonSerializer.Deserialize<PersonWithAttributes>(personString);
Console.WriteLine($"{personFromString.FirstName} {personFromString.Surname}({personFromString.IsActive}) is {personFromString.Age}");

Each property will be assigned with correct value:

John Doe(True) is 20

This is quite a simple approach in some situations and it has its pros:

  • Easy to implement
  • We can specify any name we want to use when serializing

But also, this approach comes with obvious cons: we need to set every property’s name one by one manually.

Now let’s take a look at another approach.

How Do We Set JsonNamingPolicy.CamelCase in JsonSerializerOptions?

JsonNamingPolicy.CamelCase is the built-in policy, and it reaches the serializer through the PropertyNamingPolicy property of a JsonSerializerOptions instance passed to Serialize() or Deserialize<T>().

The same instance works in both directions. Serializing writes firstName, and deserializing accepts firstName into FirstName, so one options object round-trips a camelCase payload without any attribute on the class.

The policy covers property names only. Dictionary keys have a separate DictionaryKeyPolicy, enum values need a JsonStringEnumConverter, and a property carrying [JsonPropertyName] keeps the literal name in its attribute whatever the policy says.

One habit matters more here than the naming does. Create the options instance once and reuse it, as a static readonly field or through dependency injection. JsonSerializerOptions caches the metadata it builds for each type it serializes, and constructing a fresh instance at every call throws that cache away and rebuilds it on the next call.

Let’s take a look at how to use it:

var person = new Person()
{
    Age = 20,
    FirstName = "John",
    Surname = "Doe",
    IsActive = true
};
JsonSerializer.Serialize(person, new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});

By adding a JsonSerializerOptions parameter, we provide a specific naming convention. The result will be the same as the first approach.

Pros of this approach can be:

  • Easy to implement as well
  • Camel case is a built-in naming convention
  • It comes with much more configuration controls when serializing
  • We don’t have to modify entity properties manually

When we choose this approach, some restrictions may raise our concerns:

  • It is hard and tricky to configure naming policy project-wide
  • We have to add the parameter of type JsonSerializerOptions in every method call, which is easy to forget
  • What if we want some naming conventions other than built-in ones

The next section removes the repetition. For a naming convention that is not one of the built-ins, snake_case, kebab-case, or something a partner API invented, see how to write a custom JsonNamingPolicy.

Is this material useful to you? Consider subscribing and get ASP.NET Core Web API Best Practices eBook for FREE!

Naming is only one of the things this options object decides. We can also control the order the properties are written in, leave properties out of the JSON entirely, and reach for a converter when serializing enum values as strings.

Does ASP.NET Core Use camelCase by Default?

Yes. An ASP.NET Core endpoint serializes FirstName as "firstName" without any configuration, which is why the same class produces different JSON in a console application and in a Web API.

JsonSerializerDefaults.Web is the preset responsible. Passing it to the JsonSerializerOptions constructor sets three things in one step: PropertyNamingPolicy becomes JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive becomes true, and numbers written as JSON strings are accepted.

That preset is not limited to endpoints. The System.Net.Http.Json extension methods use it too, so PostAsJsonAsync() sends camelCase and GetFromJsonAsync<T>() reads camelCase back, both without an options argument.

We can construct exactly the same options anywhere we like, including in a plain console application, by writing new JsonSerializerOptions(JsonSerializerDefaults.Web) and passing it in.

Changing the defaults for a whole application is a separate job with its own configuration hooks, and it is covered in setting global defaults for JsonSerializerOptions in ASP.NET Core.

RouteCodeScopeAffects deserialization
Attribute per property[JsonPropertyName("firstName")]That one property, everywhereYes
Options per callnew JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }Every call that passes the instanceYes
Web presetnew JsonSerializerOptions(JsonSerializerDefaults.Web)Every call that passes the instanceYes, and matching is case-insensitive
ASP.NET Core defaultsnothing to writeEvery endpoint in the applicationYes, and matching is case-insensitive

How Do We Avoid Passing JsonSerializerOptions to Every Call?

In the second approach, we must always add the parameter of type JsonSerializerOptions when we want to serialize or deserialize. That is easy to forget and quite annoying to do for every call.

In a console or library project there is no application-wide switch, so the options instance has to travel to every call. An extension method is the smallest way to stop repeating it:

public static class JsonSerializerExtensions
{
    private static readonly JsonSerializerOptions CamelCaseOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    public static string SerializeWithCamelCase<T>(this T data) =>
        JsonSerializer.Serialize(data, CamelCaseOptions);

    public static T? DeserializeFromCamelCase<T>(this string json) =>
        JsonSerializer.Deserialize<T>(json, CamelCaseOptions);
}

Then we can use our extension method to do serialization:

person.SerializeWithCamelCase();

Or deserialization:

personString.DeserializeFromCamelCase<Person>()

Both methods will give us the correct result, making our code easier to write.

Conclusion

There are three deliberate routes to camelCase, an attribute per property, a naming policy on JsonSerializerOptions, and the JsonSerializerDefaults.Web preset, and in an ASP.NET Core application the third is already switched on. For one property that needs a fixed name, use the attribute. For everything else, set the policy once on a shared options instance and reuse it.

Tested with .NET 10.0.10.