Naming & JSON Contracts
DataNormalizer generates DTOs with semantic CLR names (RouteDto, HopDto) and lets you configure JSON wire-format names independently. This separation means your C# code stays readable while your serialized output matches any existing API contract.
Naming Policy
Control DTO type names with UseNaming(). Available at two levels:
Global (applies to all graphs):
protected override void Configure(NormalizeBuilder builder)
{
builder.UseNaming(n =>
{
n.DtoSuffix = "Model"; // RouteModel instead of RouteDto
n.DtoPrefix = ""; // default — no prefix
n.ContainerSuffix = "Dto"; // SearchResponseResultDto
n.EmitJsonPropertyNames = true; // emit [JsonPropertyName] attributes
});
builder.NormalizeGraph<SearchResponse>();
}
Per-graph (overrides global for that graph):
builder.UseNaming(n => n.DtoSuffix = "Record"); // global default
builder.NormalizeGraph<SearchResponse>(graph =>
{
graph.UseNaming(n => n.DtoSuffix = "View"); // this graph uses "View"
});
Properties
| Property | Default | Effect |
|---|---|---|
DtoPrefix |
"" |
Prefix for DTO types: {Prefix}{TypeName}{Suffix} |
DtoSuffix |
"Dto" |
Suffix for DTO types. Set "" for bare names |
ContainerSuffix |
"Dto" |
Suffix for the container: {Root}Result{ContainerSuffix} |
EmitJsonPropertyNames |
true |
Emit [JsonPropertyName("camelCase")] on all generated properties |
When DtoSuffix is empty, collection property names use a List fallback (e.g., placeList instead of placeDtos). The DtoPrefix does not apply to the container type name — containers always follow {TypeName}Result{ContainerSuffix}.
JSON Contract Customization
UseJsonContract() controls the JSON property names on the container DTO itself. This is per-graph only:
builder.NormalizeGraph<SearchResponse>(graph =>
{
graph.UseJsonContract(c =>
{
c.RootPropertyName = "result";
c.Collection<Route>("routes");
c.Collection<Hop>("hops");
c.Collection<Carrier>("carriers");
c.Collection<Place>("places");
});
});
RootPropertyName
Sets the JSON name for the container's Result property. Default is "result". The Result property always gets a [JsonPropertyName] attribute regardless of the EmitJsonPropertyNames setting.
Collection<T>(jsonName)
Overrides the JSON property name for a type's list array in the container. Without it, the default name is {typeName}{DtoSuffix}s in camelCase (e.g., "routeDtos"). With Collection<Route>("routes"), it serializes as "routes".
Example JSON output
{
"result": { "routesIndices": [0], "originPlaceIndex": 0, ... },
"routes": [ { "segmentsIndices": [0], ... } ],
"hops": [ { "line": 0, "marketingCarrier": 1, ... } ],
"places": [ { "shortName": "London", "kind": "city", ... } ],
"carriers": [ { "name": "Eurostar", "code": "ES" } ]
}
Reference JSON Names
When a property is normalized into an index reference, the default JSON name is {propertyName}Index (or {propertyName}Indices for collections). Use Reference().JsonName() or ReferenceCollection().JsonName() to override this:
builder.ForType<Hop>(x =>
{
x.Reference(p => p.Line).JsonName("line");
x.Reference(p => p.MarketingCarrier).JsonName("marketingCarrier");
x.Reference(p => p.Vehicle).JsonName("vehicle");
x.ReferenceCollection(p => p.TransitImages).JsonName("transitImages");
});
This generates a DTO like:
public partial class HopDto : IEquatable<HopDto>
{
[JsonPropertyName("line")]
public int LineIndex { get; set; } // JSON: "line" instead of "lineIndex"
[JsonPropertyName("marketingCarrier")]
public int? MarketingCarrierIndex { get; set; } // JSON: "marketingCarrier"
[JsonPropertyName("vehicle")]
public int VehicleIndex { get; set; } // JSON: "vehicle"
[JsonPropertyName("transitImages")]
public int[] TransitImagesIndices { get; set; } // JSON: "transitImages"
}
The CLR property names stay semantic (LineIndex, MarketingCarrierIndex), but the wire format uses the compact names expected by the API consumer.
[NormalizeJsonName] Attribute
An attribute-based alternative to the fluent Reference().JsonName() API:
public class Hop
{
[NormalizeJsonName("line")]
public Line Line { get; set; }
[NormalizeJsonName("marketingCarrier")]
public Carrier? MarketingCarrier { get; set; }
}
Priority
When both are present, the fluent config wins:
Reference().JsonName("name")orReferenceCollection().JsonName("name")— highest priority[NormalizeJsonName("name")]— used if no fluent config exists- Default camelCase (
lineIndex,transitImagesIndices) — used if neither is set
Explicit JSON name overrides are always emitted, even when EmitJsonPropertyNames is false. This lets you turn off automatic camelCase attributes globally while still controlling specific properties.
Matching an Existing Wire Format
Combining all three features to match a transport search API:
[NormalizeConfiguration]
public partial class SearchConfig : NormalizationConfig
{
protected override void Configure(NormalizeBuilder builder)
{
// 1. Naming: keep default "Dto" suffix for CLR types
builder.UseNaming(n =>
{
n.EmitJsonPropertyNames = true; // camelCase JSON (default)
});
// 2. JSON contract: custom collection names on the container
builder.NormalizeGraph<SearchResponse>(graph =>
{
graph.UseJsonContract(c =>
{
c.RootPropertyName = "result";
c.Collection<Route>("routes");
c.Collection<Hop>("hops");
c.Collection<Line>("lines");
c.Collection<Place>("places");
c.Collection<Carrier>("carriers");
});
});
// 3. Per-property reference names on Hop
builder.ForType<Hop>(x =>
{
x.Reference(p => p.Line).JsonName("line");
x.Reference(p => p.MarketingCarrier).JsonName("marketingCarrier");
x.ReferenceCollection(p => p.TransitImages).JsonName("transitImages");
});
}
}
The result serializes as:
{
"result": { "routesIndices": [0], "originPlaceIndex": 0, "destinationPlaceIndex": 1 },
"routes": [ { "segmentsIndices": [0], "placesIndices": [0, 2] } ],
"hops": [ { "line": 0, "marketingCarrier": 0, "transitImages": [0] } ],
"lines": [ { "path": "M51.5,-0.1 L48.8,2.3", "placesIndices": [0, 2, 1] } ],
"places": [ { "shortName": "London" }, { "shortName": "Paris" }, { "shortName": "Brussels" } ],
"carriers": [ { "name": "Eurostar", "code": "ES" } ]
}
The same container round-trips through JsonSerializer.Serialize and Deserialize with no configuration needed on the System.Text.Json side.
Root Property Behavior
Every container has a Result property (C# name). Its JSON name defaults to "result" and can be changed via UseJsonContract(c => c.RootPropertyName = "data").
Whether the root type also gets a list array depends on usage:
| Scenario | Result property | Root list array |
|---|---|---|
| Root is not referenced by other types | Yes | No |
| Root is referenced by other types | Yes | Yes |
No list — SearchResponse is the root and no other type references it. The container has Result but no SearchResponseDtos array. The root DTO is only accessible through Result.
Has list — TransportNetwork is the root, but TransportLine has a Network property that references it back. Because the root type appears in another type's graph, it needs a collection for index lookups. The container has both Result and TransportNetworkDtos.
// No back-reference: Result only
var search = SearchConfig.Normalize(response);
search.Result // the root DTO
// search has no SearchResponseDtos property
// Back-reference: Result + list
var transport = TransportConfig.Normalize(network);
transport.Result // the root DTO (always index 0)
transport.TransportNetworkDtos // includes the root for index resolution
This optimization avoids a redundant array when the root is never referenced by index from another type.