Specification scope: Core with the Units and Validation companion specifications.

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

A generated schema can compile cleanly and still weaken the contract. Protocol Buffers, XML Schema (XSD), and ASN.1 have different type systems; none can recover a meaning that its vocabulary or the converter does not encode.

JSON Structure distinguishes set uniqueness, choice alternatives, numeric bounds and units, and tuple positions. Structurize reads those declarations and projects them through a target-specific mapping.

With this schema, Structurize exposes semantic losses and converter defects. The Protocol Buffers converter produces no artifact for the complete schema below; it exits with unhashable type: 'dict'. XSD emits xs:choice, but writes a Python dictionary as one alternative’s type attribute, which an independent XSD reader rejects. ASN.1 preserves SET OF, CHOICE, and tuple structure and compiles with asn1tools, but it does not carry the double range into the generated module.

Together, the target format and converter determine the projection’s semantic budget. Review it before treating a successful compile as evidence of equivalence.

One order, three projections

Use the same fulfillment contract throughout. Its complete form contains an order identifier, a delivery choice, a set of handling codes, a package-weight range in kilograms, and a tuple that records warehouse aisle and shelf:

{
  "$schema": "https://json-structure.org/meta/extended/v0/#",
  "$id": "https://schemas.example.com/fulfillment/order/v1",
  "name": "FulfillmentOrder",
  "type": "object",
  "$uses": ["JSONStructureValidation", "JSONStructureUnits"],
  "definitions": {
    "Address": {
      "name": "Address",
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "postalCode": { "type": "string" }
      },
      "required": ["street", "city", "postalCode"]
    }
  },
  "properties": {
    "orderId": { "type": "uuid" },
    "delivery": {
      "type": "choice",
      "name": "Delivery",
      "choices": {
        "ship": { "type": { "$ref": "#/definitions/Address" } },
        "collectAt": { "type": "string" }
      }
    },
    "handlingCodes": {
      "type": "set",
      "items": {
        "type": "string",
        "enum": ["fragile", "keep-dry", "upright"]
      }
    },
    "packageWeight": {
      "type": "double",
      "minimum": 0,
      "maximum": 31.5,
      "unit": "kg"
    },
    "warehouseSlot": {
      "type": "tuple",
      "name": "WarehouseSlot",
      "properties": {
        "aisle": { "type": "string" },
        "shelf": { "type": "uint16" }
      },
      "tuple": ["aisle", "shelf"]
    }
  },
  "required": ["orderId", "delivery", "handlingCodes"]
}

Generate the three artifacts independently:

structurize s2p fulfillment-order.struct.json --out generated/proto
structurize s2x fulfillment-order.struct.json --out fulfillment-order.xsd
structurize s2asn fulfillment-order.struct.json --out fulfillment-order.asn

The command names and options are defined in commands.json. Running all three commands does not promise three semantically identical contracts. Each converter preserves a different subset of the source contract.

For complex multi-file code-generation examples, use the prebuilt Avrotize Structurize gallery, which shows source schemas beside complete generated output trees. The evidence below uses the article-specific schema above and single-file schema projections.

The Protocol Buffers converter fails before producing output

Protocol Buffers does not have a native set whose type promises uniqueness, and the converter maps simpler sets to repeated. That mapping is not the result of the command shown here, however. Both the regular invocation and the --allow-optional invocation fail with unhashable type: 'dict' before writing a .proto file. The tuple property triggers a code path that treats a nested schema dictionary as a hashable primitive type.

Command output: structurize s2p
Error:  unhashable type: 'dict'

Remove or separately model the unsupported construct before evaluating how the remaining set and numeric fields project. Do not show a plausible repeated field as output from a command that produced no artifact.

Presence also needs an explicit decision. Nullable handling in this conversion requires --allow-optional:

structurize s2p fulfillment-order.struct.json \
  --out generated/proto \
  --allow-optional

Do not add that switch mechanically. Review how absent, null, and default values map into the generated API and wire representation. Similar-looking values can carry different business meaning in an order workflow.

XSD follows XML’s structure

XSD is not Protocol Buffers with angle brackets. It models XML elements, attributes, complex types, sequences, choices, and occurrence constraints. The structuretoxsd.py converter must place JSON-shaped declarations into that structure.

The converter emits xs:choice for the tagged delivery choice, despite the absence of a selector. Its ship branch is not valid XSD: the generated attribute is type="{'$ref': '#/definitions/Address'}". The .NET XSD reader reports that value as invalid. The tuple also becomes an empty xs:sequence, while the set becomes repeated item elements and loses uniqueness.

Generated output: fulfillment-order.xsd
<?xml version="1.0" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="urn:example:schema" targetNamespace="urn:example:schema" elementFormDefault="qualified">
  <xs:element name="FulfillmentOrder">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="orderId" minOccurs="1" maxOccurs="1" type="xs:string"/>
        <xs:element name="delivery" minOccurs="1" maxOccurs="1" type="Delivery"/>
        <xs:element name="handlingCodes" minOccurs="1" maxOccurs="1">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="item" minOccurs="0" maxOccurs="unbounded">
                <xs:simpleType>
                  <xs:restriction base="xs:string">
                    <xs:enumeration value="fragile"/>
                    <xs:enumeration value="keep-dry"/>
                    <xs:enumeration value="upright"/>
                  </xs:restriction>
                </xs:simpleType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
        <xs:element name="packageWeight" minOccurs="0" maxOccurs="1">
          <xs:annotation>
            <xs:appinfo source="json-structure-extensions">{
  &quot;unit&quot;: &quot;kg&quot;
}</xs:appinfo>
          </xs:annotation>
          <xs:simpleType>
            <xs:restriction base="xs:double">
              <xs:minInclusive value="0"/>
              <xs:maxInclusive value="31.5"/>
            </xs:restriction>
          </xs:simpleType>
        </xs:element>
        <xs:element name="warehouseSlot" minOccurs="0" maxOccurs="1">
          <xs:complexType>
            <xs:sequence/>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:complexType name="Delivery">
    <xs:choice>
      <xs:element name="ship" type="{'$ref': '#/definitions/Address'}"/>
      <xs:element name="collectAt" type="xs:string"/>
    </xs:choice>
  </xs:complexType>
  <xs:complexType name="Address">
    <xs:sequence>
      <xs:element name="street" minOccurs="1" maxOccurs="1" type="xs:string"/>
      <xs:element name="city" minOccurs="1" maxOccurs="1" type="xs:string"/>
      <xs:element name="postalCode" minOccurs="1" maxOccurs="1" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

Compile the generated schema with an XSD processor before validating any XML. For this exact output, that first check already fails. Converter success proves only that a file was written.

ASN.1 preserves a different subset

ASN.1 has its own structural vocabulary and encoding ecosystem. The structuretoasn1.py converter can map SET OF, CHOICE, enumerations, tuples, and references into ASN.1 constructs. The converter does not apply minimum and maximum bounds to a double, so the package-weight range is not present in that projection.

Generated output: fulfillment-order.asn
Fulfillment-order DEFINITIONS AUTOMATIC TAGS ::= BEGIN

Address ::= SEQUENCE {
    street UTF8String,
    city UTF8String,
    postalCode UTF8String
}

FulfillmentOrder ::= SEQUENCE {
    orderId UTF8String,
    delivery CHOICE { ship Address, collectAt UTF8String },
    handlingCodes SET OF ENUMERATED { fragile(0), keep-dry(1), upright(2) },
    packageWeight REAL OPTIONAL,
    warehouseSlot SEQUENCE { aisle UTF8String, shelf INTEGER (0..65535) } OPTIONAL
}

END

Useful is not universal. JSON number representations, annotations, defaults, nullability, name rules, and particular encoding choices still require review. Record the exact constructs, converter version, and encoding profile you tested. A successful conversion is not evidence of losslessness.

Test the meaning you depend on

Generated syntax validation is necessary. Compile the .proto, validate the .xsd, and parse or compile the ASN.1 module with the target toolchain. Those checks catch malformed output, unsupported syntax, and some broken references. They provide evidence of well-formed generated artifacts, not semantic equivalence.

Add projection tests around the source meanings that matter to fulfillment:

  • Submit duplicate handling codes and verify where uniqueness is enforced.
  • Exercise both delivery alternatives and reject an instance containing both.
  • Try package weights at, inside, and outside the declared range.
  • Round-trip the warehouse tuple and verify position is preserved.
  • Test absent, explicit null, and defaulted values separately.

Some checks belong in generated validators, some in adapters, and some at the service boundary. Their location is secondary. The important part is recording which semantics the target carries natively and which semantics need help.

A projection ledger can be as simple as a table in the build documentation with four columns: source construct, target representation, preserved meaning, and compensating check. Keep it beside the generator version and command line. That turns an implicit assumption into a reviewable engineering decision.

JSON Structure gives the system one authoritative contract. For each generated projection, record what the converter preserves and what it drops. A clean compile proves syntax, not equivalence.