> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dottxt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Composition

> Reuse and combine schemas.

Composition keywords let you define shared sub-schemas and combine validation rules.

## Supported keywords

| Keyword           | What it does                        |
| ----------------- | ----------------------------------- |
| `$defs` / `$ref`  | Define and reuse shared sub-schemas |
| `allOf`           | All subschemas must match           |
| `anyOf` / `oneOf` | Exactly one subschema must match    |
| `not`             | The subschema must not match        |

## Reuse with `$defs` and `$ref`

Define shared sub-schemas once, then reference them.

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "$defs": {
      "address": {
        "type": "object",
        "properties": {
          "street": { "type": "string" },
          "city": { "type": "string" },
          "zip": { "type": "string", "pattern": "^[0-9]{5}$" }
        },
        "required": ["street", "city", "zip"],
        "additionalProperties": false
      }
    },
    "type": "object",
    "properties": {
      "billing": { "$ref": "#/$defs/address" },
      "shipping": { "$ref": "#/$defs/address" }
    },
    "required": ["billing", "shipping"],
    "additionalProperties": false
  }
  ```

  ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from typing import Annotated
  from pydantic import BaseModel, ConfigDict, StringConstraints

  class Address(BaseModel):
      model_config = ConfigDict(extra="forbid")

      street: str
      city: str
      zip: Annotated[str, StringConstraints(pattern=r"^[0-9]{5}$")]

  class Payload(BaseModel):
      model_config = ConfigDict(extra="forbid")

      billing: Address
      shipping: Address
  ```

  ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  import { z } from "zod";

  const addressSchema = z
    .object({
      street: z.string(),
      city: z.string(),
      zip: z.string().regex(/^[0-9]{5}$/),
    })
    .strict();

  const payloadSchema = z
    .object({
      billing: addressSchema,
      shipping: addressSchema,
    })
    .strict();
  ```
</CodeGroup>

`$ref` support is for local references (for example `#/$defs/...`).

## `allOf`

Combine constraints that must all apply.

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "allOf": [
      {
        "type": "object",
        "properties": { "name": { "type": "string" } },
        "required": ["name"]
      },
      {
        "type": "object",
        "properties": { "email": { "type": "string", "format": "email" } },
        "required": ["email"]
      }
    ]
  }
  ```

  ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  import { z } from "zod";

  const payloadSchema = z.intersection(
    z.object({ name: z.string() }).strict(),
    z.object({ email: z.string().email() }).strict(),
  );
  ```
</CodeGroup>

<Danger>
  Pydantic example is omitted because there is no direct `allOf` keyword mapping.
</Danger>

## `anyOf` / `oneOf`

Both `anyOf` and `oneOf` behave the same way: exactly one subschema must match. Use a discriminator field plus `const` to make branches explicit.

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "oneOf": [
      {
        "type": "object",
        "properties": {
          "tool": { "const": "search" },
          "query": { "type": "string", "minLength": 1 }
        },
        "required": ["tool", "query"],
        "additionalProperties": false
      },
      {
        "type": "object",
        "properties": {
          "tool": { "const": "lookup" },
          "id": { "type": "integer" }
        },
        "required": ["tool", "id"],
        "additionalProperties": false
      }
    ]
  }
  ```

  ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from typing import Annotated, Literal
  from pydantic import BaseModel, ConfigDict, Field

  class SearchTool(BaseModel):
      model_config = ConfigDict(extra="forbid")

      tool: Literal["search"]
      query: str = Field(min_length=1)

  class LookupTool(BaseModel):
      model_config = ConfigDict(extra="forbid")

      tool: Literal["lookup"]
      id: int

  ToolCall = Annotated[SearchTool | LookupTool, Field(discriminator="tool")]
  ```

  ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  import { z } from "zod";

  const toolCallSchema = z.discriminatedUnion("tool", [
    z
      .object({
        tool: z.literal("search"),
        query: z.string().min(1),
      })
      .strict(),
    z
      .object({
        tool: z.literal("lookup"),
        id: z.number().int(),
      })
      .strict(),
  ]);
  ```
</CodeGroup>

## `not`

Reject values that match a subschema:

```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "type": "object",
  "properties": {
    "value": {
      "type": "string",
      "not": { "const": "" }
    }
  },
  "required": ["value"],
  "additionalProperties": false
}
```

<Danger>
  Pydantic and Zod examples are omitted here because there is no direct keyword mapping for JSON Schema `not`.
</Danger>

This accepts any non-empty string.

## Recursive schemas

Recursive local references are supported:

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "$defs": {
      "node": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "children": {
            "type": "array",
            "items": { "$ref": "#/$defs/node" },
            "maxItems": 10
          }
        },
        "required": ["name", "children"],
        "additionalProperties": false
      }
    },
    "type": "object",
    "properties": {
      "root": { "$ref": "#/$defs/node" }
    },
    "required": ["root"],
    "additionalProperties": false
  }
  ```

  ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from __future__ import annotations
  from pydantic import BaseModel, ConfigDict, Field

  class Node(BaseModel):
      model_config = ConfigDict(extra="forbid")

      name: str
      children: list[Node] = Field(..., max_length=10)

  class Payload(BaseModel):
      model_config = ConfigDict(extra="forbid")

      root: Node

  Payload.model_rebuild()
  ```

  ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  import { z } from "zod";

  type Node = {
    name: string;
    children: Node[];
  };

  const nodeSchema: z.ZodType<Node> = z.object({
    name: z.string(),
    children: z.array(z.lazy(() => nodeSchema)).max(10),
  }).strict();

  const payloadSchema = z.object({
    root: nodeSchema,
  }).strict();
  ```
</CodeGroup>

## Related

* [Reusability cookbook](/json-schema/reusability)
* [Recursion cookbook](/json-schema/recursion)
* [Const reference](/json-schema/reference/const)
* [Conditionals reference](/json-schema/reference/conditionals)
