Specification scope: Core only.

Tool: Structurize is the JSON Structure-focused command-line interface and conversion toolkit from the Avrotize project.

A fulfillment event permits exactly one outcome: a shipment was dispatched, or an order is ready for collection. Generated code with two nullable properties also permits both outcomes and no outcome. That larger state space violates the contract before application logic has done any useful work.

JSON Structure represents the event as a choice with a closed set of named alternatives. The declaration makes the alternatives mutually exclusive; optional fields do not.

Structurize adapts this root choice to the idioms and artifact boundaries of each target. Rust uses a data-carrying enum, Java a Jackson-compatible class hierarchy, C# a serializer-oriented wrapper, and ASN.1 its native CHOICE. For a root choice, TypeScript, Go, and Protocol Buffers emit the branch declarations rather than synthesizing a root envelope, while Parquet and Iceberg require an object-shaped root. Each mapping follows the target’s type system, serialization model, or document shape. The declarations are therefore expected to differ. The sections below show which commands produce a complete root representation and which produce components for an application-defined root.

One event, two possible outcomes

Consider a fulfillment event that reports either a dispatched shipment or an order ready for collection. The tagged form carries the selected alternative as the single property of a wrapper object:

{
  "$schema": "https://json-structure.org/meta/core/v0/#",
  "$id": "https://schemas.example.com/fulfillment-event.json",
  "name": "FulfillmentEvent",
  "type": "choice",
  "choices": {
    "shipmentDispatched": {
      "type": { "$ref": "#/definitions/ShipmentDispatched" }
    },
    "pickupReady": {
      "type": { "$ref": "#/definitions/PickupReady" }
    }
  },
  "definitions": {
    "ShipmentDispatched": {
      "type": "object",
      "name": "ShipmentDispatched",
      "properties": {
        "fulfillmentId": { "type": "uuid" },
        "trackingCode": { "type": "string" }
      },
      "required": ["fulfillmentId", "trackingCode"]
    },
    "PickupReady": {
      "type": "object",
      "name": "PickupReady",
      "properties": {
        "fulfillmentId": { "type": "uuid" },
        "pickupLocation": { "type": "string" }
      },
      "required": ["fulfillmentId", "pickupLocation"]
    }
  }
}

The JSON value makes the branch explicit:

{
  "shipmentDispatched": {
    "fulfillmentId": "4d616677-5a1b-4d1d-86df-1fc4a0236bc8",
    "trackingCode": "ZK-20418"
  }
}

The object represents one value whose type is the selected alternative.

Sum types have several dialects

In the Avrotize implementation, the five language commands used here are registered as s2cs, s2java, s2ts, s2go, and s2rust in the command registry. Run the five registered language commands:

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

Compare the resulting type shapes. The C# converter projects the choice as a wrapper with nullable object members. Java’s choice template uses nested alternative classes named after the branches. The TypeScript converter can return a union expression while resolving a choice. For this root choice, the command emits ShipmentDispatched.ts and PickupReady.ts. Rust’s converter emits enum variants.

For a complete multi-file code-generation example, the prebuilt Avrotize Inventory to C# gallery example shows its source schema and output tree. For the small schema here, each expandable section shows one representative generated file.

These excerpts come from the generated C#, Java, and Rust files:

Generated output: FulfillmentEvent.cs
public partial class FulfillmentEvent
{
  /// <summary>
  /// Gets or sets the ShipmentDispatched value
  /// </summary>
  public object? ShipmentDispatched { get; set; } = null;
  /// <summary>
  /// Gets or sets the PickupReady value
  /// </summary>
  public object? PickupReady { get; set; } = null;
Generated output: FulfillmentEvent.java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT, property = "type")
@JsonSubTypes({
  @JsonSubTypes.Type(value = ShipmentDispatched.class, name = "shipmentDispatched"),
  @JsonSubTypes.Type(value = PickupReady.class, name = "pickupReady")
})
public abstract class FulfillmentEvent {
  public FulfillmentEvent() {}

  /** shipmentDispatched variant */
  public static class ShipmentDispatched extends FulfillmentEvent {
    private java.ShipmentDispatched value;

    public ShipmentDispatched() {}

    public ShipmentDispatched(java.ShipmentDispatched value) {
      this.value = value;
    }

    public java.ShipmentDispatched getValue() { return value; }
    public void setValue(java.ShipmentDispatched value) { this.value = value; }
  }

  /** pickupReady variant */
  public static class PickupReady extends FulfillmentEvent {
    private java.PickupReady value;

    public PickupReady() {}

    public PickupReady(java.PickupReady value) {
      this.value = value;
    }

    public java.PickupReady getValue() { return value; }
    public void setValue(java.PickupReady value) { this.value = value; }
  }
}
Generated output: fulfillmentevent.rs
pub enum FulfillmentEvent {
    ShipmentDispatched(ShipmentDispatched),
    PickupReady(PickupReady)
}

Each generator exposes the choice through conventions appropriate to its target. Rust materializes the root as an enum and Java as a Jackson hierarchy. C# uses a mutable wrapper, while TypeScript and Go emit reusable branch types without synthesizing a root declaration. The first group provides a root API; the second provides the branch components from which an application can define one according to its serialization conventions.

The Java tests, TypeScript tests, and Rust tests exercise those generators.

The C# wrapper itself permits both members or neither. Its reader loops over properties and can accept more than one recognized branch. Its writer chooses the first non-null branch in generated branch order. The C# tests cover the generator, but the schema still forbids states that the generated class permits. In this example, both C# branch payloads resolve to object. The generated overloads FulfillmentEvent(object shipmentdispatched) and FulfillmentEvent(object pickupready) therefore have the same CLR signature; parameter names do not distinguish overloads. A C# integration can use the mutable properties directly or introduce distinct payload types so that the constructor signatures differ. This constraint is separate from the decision to represent the choice as a serializer-oriented wrapper.

Root choices across target formats

TypeScript and Go emit the branch declarations without synthesizing a root envelope. An application that needs such an envelope can define one according to its serialization and validation conventions.

Protocol Buffers has a oneof construct, but the structuretoproto.py converter emits the two branch messages for this root choice rather than synthesizing an enclosing oneof:

Generated output: proto.proto
message ShipmentDispatched {
  string fulfillmentId = 1;
  string trackingCode = 2;
}

message PickupReady {
  string fulfillmentId = 1;
  string pickupLocation = 2;
}

ASN.1 represents the root branch set with CHOICE:

Generated output: fulfillment-event.asn
FulfillmentEvent ::= CHOICE { shipmentDispatched ShipmentDispatched, pickupReady PickupReady }

The Parquet and Iceberg converters require an object-shaped row root. For this input, both report Expected a JSON Structure schema with type 'object' at the top level. Wrap the choice in a root object when evaluating their nested-choice mapping.

Evaluate each target contract

The Rust enum, Java hierarchy, C# wrapper, and ASN.1 CHOICE are target-specific root representations. The TypeScript, Go, and Protocol Buffers outputs provide branch declarations for a target-specific root defined by the application. Review each result for its intended wire behavior, construction rules, and tooling integration.

The JSON Structure declaration remains the common contract. Target-specific code can enforce additional construction rules or supply an envelope where the target integration requires one, without changing the source choice.