Specification scope: Core with the Alternate Names companion specification.
Tool: Structurize is the JSON Structure-focused command-line interface and conversion toolkit from the Avrotize project.
A published JSON key is part of the wire contract. C#, Java, Go, Rust, and TypeScript use different case conventions and reserve different words. Letting those rules choose the wire spelling turns routine code generation into a protocol-change mechanism.
JSON Structure keeps the property name as its contract identity and uses
altnames
when its JSON representation needs another spelling. That mapping lets
fulfillmentId describe the property while fulfillment_id remains the
published wire key.
Structurize sanitizes and case-converts identifiers for each language. With the target’s serializer annotations enabled, it binds those identifiers back to the declared JSON name. The code spelling changes while the wire spelling survives. In the tested outputs, generators omit that mapping unless serializer annotations are enabled.
The wire already has a spelling
Suppose the fulfillment API has published fulfillment_id and
pickup_location, while the schema uses its regular identifiers for references
and required-property declarations:
{
"$schema": "https://json-structure.org/meta/extended/v0/#",
"$id": "https://schemas.example.com/fulfillment-event.json",
"name": "FulfillmentEventSchema",
"$uses": ["JSONStructureAlternateNames"],
"$root": "#/definitions/PickupReady",
"definitions": {
"PickupReady": {
"name": "PickupReady",
"type": "object",
"properties": {
"fulfillmentId": {
"type": "uuid",
"altnames": { "json": "fulfillment_id" }
},
"pickupLocation": {
"type": "string",
"altnames": { "json": "pickup_location" }
},
"readyAt": {
"type": "datetime",
"altnames": { "json": "ready_at" }
}
},
"required": ["fulfillmentId", "pickupLocation", "readyAt"],
"additionalProperties": false
}
}
}
The schema still requires fulfillmentId. An extension-aware JSON serializer
uses the reserved json alternate name and emits this representation:
{
"fulfillment_id": "4d616677-5a1b-4d1d-86df-1fc4a0236bc8",
"pickup_location": "SEA-042",
"ready_at": "2026-09-29T16:30:00Z"
}
The article One Property, Several Names explains what the annotation means in the schema. Here the concern is narrower: whether generated serializers preserve that meaning after identifiers have been adapted to a programming language.
Annotation flags carry the mapping
The alternate-name tests cover JSON wire names for C#, Java, TypeScript, Go, and Rust when their serializer annotations are enabled, including nested collection values. The relevant CLI forms are:
structurize s2cs fulfillment-event.struct.json --out generated/csharp \
--system_text_json_annotation
structurize s2java fulfillment-event.struct.json --out generated/java
structurize s2ts fulfillment-event.struct.json --out generated/typescript \
--typedjson-annotation
structurize s2go fulfillment-event.struct.json --out generated/go \
--json-annotation
structurize s2rust fulfillment-event.struct.json --out generated/rust \
--json-annotation
Java uses Jackson by default in this converter. The switches are recorded
in the command registry.
The underscore spelling of --system_text_json_annotation is exact.
Without the serializer mode, a generator may still produce legal code names,
but the generated serializer has no obligation to emit the alternate JSON key.
Record that difference in build configuration instead of relying on
undocumented team knowledge.
For a complete multi-file code-generation example, the prebuilt Avrotize Inventory to C# gallery example shows its source schema and output tree. The disclosures below use the small article-specific schema and one representative file per target.
The generated forms are idiomatic for their ecosystems. Condensed to one property, they carry the same mapping:
Generated output: PickupReady.cs
[System.Text.Json.Serialization.JsonPropertyName("fulfillment_id")]
public required Guid fulfillmentId { get; set; }
Generated output: PickupReady.java
@JsonProperty("fulfillment_id")
private UUID fulfillmentId;
Generated output: PickupReady.ts
@jsonMember(String, { name: 'fulfillment_id' })
public fulfillmentId: string;
Generated output: PickupReady.go
FulfillmentId string `json:"fulfillment_id"`
Generated output: pickupready.rs
pub fulfillment_id: uuid::Uuid,
The Rust output has no rename attribute because its snake-case identifier is
already the declared wire spelling. C#, Java, TypeScript, and Go carry explicit
metadata because their member identifiers differ. The current TypeScript
template passes the wire name through TypedJSON’s name option, and the
Go template
puts it in a json struct tag.
Sanitization is not a contract rename
Even without altnames, code generation must cope with target conventions.
PascalCase properties are normal in C#. Exported Go fields begin with an upper-
case letter. Rust commonly uses snake case. Reserved words and otherwise
illegal identifiers need sanitization. Those transformations create usable
source code; they do not authorize a serializer to improvise a new JSON key.
In generated C#, changing the member from fulfillmentId to FulfillmentId
may be harmless. Changing the wire key from fulfillment_id to
FulfillmentId breaks interoperability unless the contract changed. Looking
only at the class declaration hides that difference; inspect the annotation and
serialized output as well.
Test deserialization separately. It must accept the declared wire name and populate the generated member. A round-trip test that serializes and then deserializes its own output is useful, but insufficient: both halves can agree on the same wrong spelling. Include a fixture written directly from the JSON Structure contract.
Purpose keys are not universal generator switches
The reserved json purpose has defined meaning in the alternate-names
extension. Custom purposes do not acquire universal behavior merely because
they appear in altnames. A key such as warehouse, protobuf, or sql is an
application-defined annotation unless a particular tool explicitly documents
how it uses that purpose.
In particular, an altnames.sql entry is not guaranteed to control SQL
generation. The tests verify that a non-JSON purpose does not replace the
JSON wire name. A custom purpose needs a policy that defines its meaning and a
tool that implements that policy; the purpose-key name alone is not a storage
contract.
Test the serialized contract
For each generated language, keep a small contract fixture and assert the exact JSON keys. Also inspect the generated identifier, because sanitization can create collisions: two distinct schema names may collapse to one code spelling after case conversion. The generator needs a deterministic response rather than a silent merge.
Do not edit generated annotations as the primary fix. Regeneration will erase the repair, and another language will still be wrong. Correct the JSON Structure annotation or the generator configuration, regenerate, and test the wire form.
A codebase may have five idiomatic names for one property. That is fine. The wire has one declared name, and the JSON Structure schema remains the source contract that tells every generated serializer what it is.