Skip to main content
When the same structure appears in multiple places, such as billing and shipping addresses, home and work phone numbers, or primary and secondary contacts, copy-pasting the definition is fragile. Change one copy and forget the other, and you have two subtly different schemas that should be identical. $defs and $ref solve this. You define the structure once in $defs and reference it wherever it’s needed. Updates happen in one place, and every reference stays in sync.

Use case

An order output with billing_address and shipping_address that must follow exactly the same structure: the same fields, the same constraints, and the same required list.

Schema pattern

{
  "type": "object",
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "line1": { "type": "string", "minLength": 1, "maxLength": 120 },
        "line2": { "type": "string", "maxLength": 120 },
        "city": { "type": "string", "minLength": 1, "maxLength": 80 },
        "postal_code": { "type": "string", "minLength": 3, "maxLength": 20 },
        "country": { "type": "string", "pattern": "^[A-Z]{2}$" }
      },
      "required": ["line1", "city", "postal_code", "country"],
      "additionalProperties": false
    }
  },
  "properties": {
    "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" },
    "billing_address": { "$ref": "#/$defs/address" },
    "shipping_address": { "$ref": "#/$defs/address" }
  },
  "required": ["order_id", "billing_address", "shipping_address"],
  "additionalProperties": false
}

Example output

{
  "order_id": "ORD-8391",
  "billing_address": {
    "line1": "10 Main St",
    "city": "Austin",
    "postal_code": "78701",
    "country": "US"
  },
  "shipping_address": {
    "line1": "55 River Rd",
    "city": "Austin",
    "postal_code": "78702",
    "country": "US"
  }
}

Why this works

Both billing_address and shipping_address resolve to the same $defs/address definition. If you later need to add a state field or change postal_code to zip, you change it once and both addresses update. This also improves readability. A schema with ten $ref references to a well-named definition is easier to review than ten inlined copies of the same 15-line object. Reviewers can focus on the top-level structure and drill into definitions only when needed. In Zod 4, reuse in code does not automatically become $defs reuse in generated JSON Schema. Pass reused: "ref" to toJSONSchema(...) to extract repeated schemas into $defs, and add metadata like .meta({ id: "addressSchema" }) if you want a stable definition name instead of an auto-generated one.