Data and Formats · Lesson 22

Property attributes override a general naming policy

Preserve a stable external name for one member while applying a broader convention.

How it works

A general naming policy provides a consistent convention, such as camel case, for many model properties. Some external schemas still require a specific spelling for an individual member. JsonPropertyName supplies that explicit mapping, so Id appears as user_id while DisplayName follows the general camel-case policy.

The output demonstrates both rules in the same object. The attribute is not a comment or a suggestion for display; it participates in the serialization contract. Renaming the C# property can preserve the wire spelling when the attribute remains, which is useful when an internal refactor must not break an established external schema.

Keep external names distinct from domain meanings. A field called user_id may contain an integer in this small example, but another schema could use a string identifier. The attribute maps a name, not a type conversion or validation rule. The property's type and boundary validation still determine what values are acceptable.

A common mistake is changing a global naming policy to solve one legacy field and unintentionally changing every other property. A narrow attribute expresses the exception more clearly. Conversely, hundreds of repetitive attributes may indicate that a consistent policy would better describe the schema as a whole.

Practice by deserializing the emitted JSON and checking both member values. Then try a payload using Id instead of user_id and inspect the result under the current options. Add required presence or domain validation if a silently unpopulated identifier would be dangerous for the application.

Finally, write a schema-focused test using the external property names rather than only round-tripping one object through the same serializer configuration. A round trip can hide a coordinated breaking change because the writer and reader make the same mistake. Testing against a small agreed wire example protects compatibility with independent producers and consumers.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
Console.WriteLine(JsonSerializer.Serialize(new Member { Id = 7, DisplayName = "Rune" }, options));
class Member {
    [JsonPropertyName("user_id")]
    public int Id { get; init; }
    public string DisplayName { get; init; } = "";
}

Expected output

{"user_id":7,"displayName":"Rune"}
Try this
The explicit JsonPropertyName is the external contract for that member.