Skip to main content
Composition keywords let you define shared sub-schemas and combine validation rules.

Supported keywords

KeywordWhat it does
$defs / $refDefine and reuse shared sub-schemas
allOfAll subschemas must match
anyOf / oneOfExactly one subschema must match
notThe subschema must not match

Reuse with $defs and $ref

Define shared sub-schemas once, then reference them.
{
  "$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
}
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
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();
$ref support is for local references (for example #/$defs/...).

allOf

Combine constraints that must all apply.
{
  "allOf": [
    {
      "type": "object",
      "properties": { "name": { "type": "string" } },
      "required": ["name"]
    },
    {
      "type": "object",
      "properties": { "email": { "type": "string", "format": "email" } },
      "required": ["email"]
    }
  ]
}
import { z } from "zod";

const payloadSchema = z.intersection(
  z.object({ name: z.string() }).strict(),
  z.object({ email: z.string().email() }).strict(),
);
Pydantic example is omitted because there is no direct allOf keyword mapping.

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.
{
  "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
    }
  ]
}
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")]
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(),
]);

not

Reject values that match a subschema:
JSON Schema
{
  "type": "object",
  "properties": {
    "value": {
      "type": "string",
      "not": { "const": "" }
    }
  },
  "required": ["value"],
  "additionalProperties": false
}
Pydantic and Zod examples are omitted here because there is no direct keyword mapping for JSON Schema not.
This accepts any non-empty string.

Recursive schemas

Recursive local references are supported:
{
  "$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
}
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()
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();