Updated on
JsonNamingPolicy is the System.Text.Json type that converts a C# property name into a JSON key. We assign one to JsonSerializerOptions.PropertyNamingPolicy, and the serializer calls it for every property it writes.
Five policies are built in: CamelCase, SnakeCaseLower, SnakeCaseUpper, KebabCaseLower and KebabCaseUpper. The four snake and kebab policies arrived in .NET 8. There is no PascalCase policy, because PascalCase is what the serializer already does when PropertyNamingPolicy is null.
When none of the five matches the convention a partner API expects, we write our own: derive from JsonNamingPolicy, override ConvertName(), and return the name we want.
Let’s take a look.
Why Would We Change JSON Property Names in C#?
JsonSerializer.Serialize() writes each JSON key exactly as the C# property is declared, because JsonSerializerOptions.PropertyNamingPolicy is null until we set it.
That is fine while both ends of the wire are C#. It stops being fine the moment the other end is not. A JavaScript client expects givenName, a Python or Ruby service expects given_name, and a URL-flavoured API may want given-name. None of those is what a C# GivenName property produces on its own.
The verbatim copy is also unforgiving. A property accidentally declared surName serializes to "surName", lower s and all, because the serializer is not correcting anything. It reproduces the member name.
So there are two reasons to change the names, and they need different tools. Matching a convention the whole payload follows is a naming policy. Fixing one property whose JSON name simply differs from its C# name is an attribute.
Let’s see that in action, but with a twist, we start one of the properties with a lowercase letter to see how it affects the output:
public class Person
{
public string? GivenName { get; set; }
public string? surName { get; set; }
}
var person = new Person()
{
GivenName = "Name1",
surName = "Surname1"
};
var jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString);
Here we create a simple Person object, serialize it, and write the output to the console:
{"GivenName":"Name1","surName":"Surname1"}
As we can see, the output follows the class definition, where the property name determines the JSON key name. Note the key “surName” has a lowercase “s” the same as the property from the Person class.
What if we need to talk to an API or service defined using a different language, and therefore need to provide a JSON file using a different naming policy? Well, that’s why we can create custom naming policies.
A naming policy changes what the keys are called. If we need to control the order the properties are written in, rather than their names, that is a separate setting.
System.Text.Json.How Do We Implement a Custom JsonNamingPolicy?
A custom policy is a class that derives from the abstract JsonNamingPolicy and overrides one method: public override string ConvertName(string name).
The serializer hands ConvertName() the C# member name and uses whatever string comes back as the JSON key. That is the entire contract. There is no attribute to apply and no registration step, the class is assigned to JsonSerializerOptions.PropertyNamingPolicy and it is in effect for every property serialized through those options.
Two rules keep a custom policy out of trouble. It must be deterministic, because the same property has to produce the same key on every call. And it must not produce the same key for two different properties: rather than writing a duplicate name, JsonSerializer throws an InvalidOperationException telling us which property collided.
Reach for one only when none of the five built-in policies fits. Hand-writing a camelCase policy when JsonNamingPolicy.CamelCase exists is a way to reintroduce a bug the framework already fixed.
The cost of a custom policy is smaller than it looks. The serializer calls ConvertName() once per property and caches the result on the options instance, so a custom policy costs nothing per serialization, provided the options object is reused.
Before we implement a custom naming policy let’s take a moment to review some common naming conventions.
What Is a Naming Convention?
It’s a common practice for formatting the names of variables, types, methods, and anything else in a language. So, let’s look at some of the different naming conventions:
- flatcase – all lowercase
- UPPERCASE – all uppercase
- camelCaseConvention – the first letter of the first word is lowercase, other first letters are uppercase
- PascalCaseConvention – the first letter of each word is uppercase
- snake_case_convention – an underscore (_) character between each word
- kebab-case-convention – a hyphen (-) character between each word
We can also mix and match conventions, for example, Kebab-Pascal-Case-Convention and Snake_Pascal_Case_Convention exist. Also, naming conventions have multiple names, for example, UPPERCASE goes by CONSTANTCASE, or even SCREAMINGCASE.
An important thing to note is that except for naming conventions such as the flatcase and UPPERCASE, we have a way to differentiate the start of a new word. This is an important feature of naming conventions and plays a significant role in implementing a custom naming policy.
How Do We Override ConvertName()?
We create a custom naming policy by implementing the abstract JsonNamingPolicy class, which has a few built-in static properties for common naming policies that we will discuss later. For our JsonNamingPolicy class we override the abstract ConvertName() method to implement our custom naming policy.
Microsoft’s System.Text.Json documentation states the same contract: “To use a custom JSON property naming policy, create a class that derives from JsonNamingPolicy and override the ConvertName method”.
Let’s keep our examples simple and implement “camelCase” and “node/separator” policies. The “camelCase” policy is in common use on the internet, and the “node/separator” policy is fictional, however, it showcases manipulating our properties at every new word.
Let’s look at the “camelCase” implementation, and this one is for illustration only. The built-in JsonNamingPolicy.CamelCase policy handles cases this hand-rolled version does not, such as IDNumber, which the built-in policy writes as idNumber and this version writes as iDNumber. Write a policy like this one only for a convention the framework does not ship.
public class CamelCasePolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
return char.IsUpper(name[0]) ? char.ToLower(name[0]) + name[1..] : name;
}
}
We look at the first character and lowercase it if needed; otherwise, we return the parameter unchanged.
Next, let’s look at the “node/separator” implementation:
public class NodeSeparatorPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
ArgumentNullException.ThrowIfNull(name);
var sb = new StringBuilder();
sb.Append(char.ToLower(name[0]));
for (int i = 1; i < name.Length; i++)
{
if (char.IsUpper(name[i]))
{
sb.Append($"/{char.ToLower(name[i])}");
}
else
{
sb.Append(name[i]);
}
}
return sb.ToString();
}
}
First, we test for a null value, then we lowercase the first letter and add that character to a StringBuilder. From there we loop through the rest of the characters and anytime we find an uppercase character we add a forward slash (/) and a lowercase version of the character to the StringBuilder. Now that we have our custom naming policies let’s see them in action.
How Do We Apply a Naming Policy With JsonSerializerOptions?
After creating our custom naming policies we should use them. So, to do that we need to create a JsonSerializerOptions class and assign a new instance of our policy to the PropertyNamingPolicy property.
Let’s see that in action for both the CamelCasePolicy and the NodeSeparatorPolicy:
var person = new Person()
{
GivenName = "Name1",
surName = "Surname1"
};
var camelCaseOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = new CamelCasePolicy()
};
jsonString = JsonSerializer.Serialize(person, camelCaseOptions);
Console.WriteLine(jsonString);
var nodeOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = new NodeSeparatorPolicy()
};
jsonString = JsonSerializer.Serialize(person, nodeOptions);
Console.WriteLine(jsonString);
In our example, we instantiate a person object and serialize it with our policies resulting in this output:
{"givenName":"Name1","surName":"Surname1"}
{"given/name":"Name1","sur/name":"Surname1"}
The first output uses the CamelCasePolicy and the second uses the NodeSeparatorPolicy. That’s all we need to do.
Build the options object once and reuse it: JsonSerializerOptions is designed to be shared, and a fresh instance at every call discards the metadata it has cached. In an ASP.NET Core application we can go further and set the policy once for the whole application.
Which Naming Policies Are Built Into JsonNamingPolicy?
JsonNamingPolicy exposes five ready-made policies as static properties, and using one is a single assignment to PropertyNamingPolicy, with no class to write.
CamelCase has been there since .NET Core 3.0. SnakeCaseLower, SnakeCaseUpper, KebabCaseLower and KebabCaseUpper arrived in .NET 8, and they are the ones worth knowing about, because snake_case and kebab-case are what most non-.NET APIs use and hand-rolling either of them is fiddly.
The snake policies join words with an underscore and the kebab policies with a hyphen. The Lower and Upper suffixes decide the casing of the result, not of the separator.
These are properties, not constructors, so there is nothing to instantiate and nothing to cache. We assign the property itself.
One name is missing from the list on purpose. There is no PascalCase policy, because that is what the serializer already produces when no policy is set at all.
| Policy | PropertyName becomes | Introduced |
|---|---|---|
JsonNamingPolicy.CamelCase | propertyName | .NET Core 3.0 |
JsonNamingPolicy.SnakeCaseLower | property_name | .NET 8 |
JsonNamingPolicy.SnakeCaseUpper | PROPERTY_NAME | .NET 8 |
JsonNamingPolicy.KebabCaseLower | property-name | .NET 8 |
JsonNamingPolicy.KebabCaseUpper | PROPERTY-NAME | .NET 8 |
| (no policy, the default) | PropertyName | n/a |
That absence is worth stating plainly, because it is a common search: leaving PropertyNamingPolicy unset writes the property name exactly as declared, which for conventional C# is already PascalCase, so a JsonNamingPolicy.PascalCase would have nothing left to do.
The five properties are listed on the Microsoft documentation page for the type.
The kebab cases use a dash (-) between words and have upper- and lowercase variations, whereas the snake cases use an underscore (_) between words and also have upper- and lowercase variations.
Since these are static properties we can assign them directly to the PropertyNamingPolicy property:
var snakeCaseLowerPolicy = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
If we use this with our person object from earlier, we see the properties formatted as snake-case:
{"given_name":"Name1","sur_name":"Surname1"}
Does JsonPropertyName Override the Naming Policy?
Yes. [JsonPropertyName("given_name")] on a property wins over whatever PropertyNamingPolicy is set to, and the policy is not applied to that property at all.
That makes the two features complementary rather than competing. The policy states the rule the payload follows; the attribute states the exceptions. A DTO whose JSON is snake_case except for one legacy field named id_ is a naming policy plus one attribute, not a custom policy with a special case compiled into it.
The reach of a policy is narrower than it first appears. PropertyNamingPolicy renames property names and nothing else.
Dictionary keys are governed separately, by DictionaryKeyPolicy, so a Dictionary<string, int> keeps its keys verbatim unless that property is set too. Enum values written as strings are the converter’s business: JsonStringEnumConverter takes its own naming policy, and does not inherit the one on the options object.
Reading works the same way. The attribute’s name is the only one accepted, so a payload using the policy-derived name does not bind.
| Name in the JSON | Controlled by | Naming policy applies |
|---|---|---|
| Property names | PropertyNamingPolicy | Yes |
A property with [JsonPropertyName("x")] | the attribute | No, the attribute wins |
Dictionary<string, T> keys | DictionaryKeyPolicy | Only if that property is also set |
| Enum values serialized as strings | JsonStringEnumConverter | Only via the converter's own policy argument |
DictionaryKeyPolicy is one-directional as well: it converts keys when we write JSON and is ignored when we read it, so a dictionary deserializes against the keys exactly as the payload spells them. The System.Text.Json documentation puts it in one line: “Naming policies for dictionary keys apply to serialization only.”
Two of those rows have articles of their own: serializing enums as strings, and how to leave a property out of the JSON entirely.
Conclusion
For camelCase, snake_case or kebab-case, assign one of the five built-in JsonNamingPolicy properties to PropertyNamingPolicy and stop there. Write a custom policy only for a convention the framework does not ship: derive from JsonNamingPolicy, override ConvertName(), and keep it deterministic. For a single property whose JSON name is simply different, use [JsonPropertyName] instead of bending the policy around it.
Tested with .NET 10.0.10.
