Specification scope: Core with the Validation companion specification.
Tool: Structurize is the JSON Structure-focused command-line interface and conversion toolkit from the Avrotize project.
Two properties can contain objects with identical members and still denote different concepts. Conversely, two references to one definition denote the same contract even when generated code places the declaration in another file or namespace. Copying the object shape into every use site erases that identity.
JSON Structure assigns compound declarations stable locations under
definitions
and connects use sites with local
$ref
values. Structurize emits named declarations for the example in several
targets, but it does not make every generated property refer to them. Java, Go,
and Rust preserve the Parcel property type. C# emits Parcel.cs yet types the
property as object; TypeScript emits neither a Parcel class nor a useful
reference.
Target naming rules may change the package, module, case, or legal identifier.
Whether the referenced identity survives is a converter behavior to test, not
a guarantee to infer from the source $ref. In this tested input, primitive
references become object, Object, interface{}, or
serde_json::Value rather than constrained strings.
Reuse is visible in the source
Continue the fulfillment model with a reusable parcel and tracking code. The
event and the parcel both refer to the same TrackingCode definition:
{
"$schema": "https://json-structure.org/meta/validation/v0/#",
"$id": "https://schemas.example.com/fulfillment-event.json",
"name": "FulfillmentEventSchema",
"$uses": ["JSONStructureValidation"],
"$root": "#/definitions/Events/ShipmentDispatched",
"definitions": {
"Common": {
"TrackingCode": {
"type": "string",
"pattern": "^[A-Z]{2}-[0-9]{5}$"
},
"Parcel": {
"name": "Parcel",
"type": "object",
"properties": {
"parcelId": { "type": "uuid" },
"trackingCode": {
"type": { "$ref": "#/definitions/Common/TrackingCode" }
}
},
"required": ["parcelId", "trackingCode"]
}
},
"Events": {
"ShipmentDispatched": {
"name": "ShipmentDispatched",
"type": "object",
"properties": {
"fulfillmentId": { "type": "uuid" },
"trackingCode": {
"type": { "$ref": "#/definitions/Common/TrackingCode" }
},
"parcel": {
"type": { "$ref": "#/definitions/Common/Parcel" }
}
},
"required": ["fulfillmentId", "trackingCode", "parcel"]
}
}
}
}
The paths identify Common/TrackingCode and Common/Parcel, distinguish them
from declarations in other namespaces, and give every use site one stable
destination. The earlier article
Definitions Are a Type Library
explains those schema semantics. The remaining question is how a generator
preserves those identities when a target language cannot use JSON Pointers as
type names.
A compound reference may become a target-language reference
Run the same source through the registered generators:
structurize s2cs fulfillment-event.struct.json --out generated/csharp
structurize s2java fulfillment-event.struct.json --out generated/java
structurize s2ts fulfillment-event.struct.json --out generated/typescript
structurize s2go fulfillment-event.struct.json --out generated/go
structurize s2rust fulfillment-event.struct.json --out generated/rust
Those commands are defined in the Avrotize command registry. 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 files give a mixed answer. Java declares
private java.common.Parcel parcel;, Go declares Parcel CommonParcel, and
Rust declares pub parcel: crate::common::parcel::Parcel. Those three use sites
retain a named compound type.
C# writes Common/Parcel.cs, but the generated ShipmentDispatched property is
public required object parcel. The declaration exists without a typed use
site. TypeScript writes empty Common and Events classes and no Parcel
declaration. The same source identity therefore survives in three targets,
degrades in one, and disappears in another. The command list alone does not
promise more.
Generated output: ShipmentDispatched.cs
/// <summary>
/// trackingCode
/// </summary>
public required object trackingCode { get; set; }
/// <summary>
/// parcel
/// </summary>
public required object parcel { get; set; }
Generated output: ShipmentDispatched.java
/** trackingCode */
private Object trackingCode;
public Object getTrackingCode() { return trackingCode; }
public void setTrackingCode(Object trackingCode) { this.trackingCode = trackingCode; }
/** parcel */
private java.common.Parcel parcel;
public java.common.Parcel getParcel() { return parcel; }
public void setParcel(java.common.Parcel parcel) { this.parcel = parcel; }
Generated output: EventsShipmentDispatched.go
// ShipmentDispatched
type EventsShipmentDispatched struct {
FulfillmentId string
TrackingCode interface{}
Parcel CommonParcel
}
Generated output: shipmentdispatched.rs
/// ShipmentDispatched
#[derive(Debug, PartialEq, Clone, Default)]
pub struct ShipmentDispatched {
pub fulfillment_id: uuid::Uuid,
pub tracking_code: serde_json::Value,
pub parcel: crate::common::parcel::Parcel,
}
Generated output: Events.ts
/** A Events class. */
export class Events {
constructor(
) {
}
/**
* Creates an instance of Events with sample data for testing.
* @returns A new Events instance with sample values.
*/
public static createInstance(): Events {
return new Events(
);
}
}
TrackingCode marks a broader current limitation. It becomes object in C#,
Object in Java, interface{} in Go, and serde_json::Value in Rust; no
generated wrapper carries the string pattern. Keep constraints such as the
tracking-code pattern in schema validation, and test each generated target
before treating primitive definitions as domain types.
Namespace trees do not travel unchanged
Common/Parcel is a path through nested JSON objects. It is not a universal
package spelling. C# may use namespaces, Java packages, TypeScript modules,
Go packages, and Rust modules, but their rules and generator layouts differ.
Some generators flatten part of the hierarchy; others preserve more of it.
The implementation tree
defines the current projection; JSON Structure defines no single namespace
mapping across these languages.
Give two schema namespaces their own Parcel definition. The schema
distinguishes them by full pointer:
#/definitions/Inbound/Parcel
#/definitions/Outbound/Parcel
A target that flattens both names must disambiguate them. Prefixing, qualification, or another deterministic collision policy can work. Silently merging the declarations cannot, because the source says they are different types even if their current properties happen to match.
Legal identifiers are projections too
Target languages reserve words and impose naming conventions. Generators
therefore sanitize identifiers and convert case. A schema property such as
pickup_location may become PickupLocation in C#, pickupLocation in Java,
and pickup_location in Rust. A definition that collides with a reserved word
needs another legal spelling.
This transformation is necessary, but it creates two identities to track:
- The schema path identifies the contract declaration.
- The generated identifier identifies its projection in one target.
My recommendation is to require a deterministic mapping and serializer metadata that preserves the wire spelling. A manual rename is temporary because regeneration applies the generator’s policy again. When a target name matters, record the generator configuration and version in the build, then test the public generated surface.
Verify every generated reference
A useful review starts from references, not files. For each local $ref, find
the corresponding generated declaration and verify that all use sites point to
it. Then check namespace collisions, case conversion, reserved words, and the
constraints that a target type cannot express.
Generated declarations may move between packages or acquire sanitized names as
tooling evolves. #/definitions/Common/Parcel remains the contract identity.
Test that every generated use site maps back to that declaration.