> ## 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.

# Union of Objects

> Use discriminated unions to route output into one valid object shape.

When output can take multiple object shapes, each shape should define its own required fields and constraints. If you use one flat object with mostly optional fields, the model can mix fields across shapes and produce ambiguous payloads.

A discriminated `anyOf` schema solves this: each branch defines exactly one object variant, and the model must pick one branch and fill it completely. Your runtime reads the discriminator and dispatches without heuristics.

## Common use case: agent tool routing

A support agent that can search docs, look up an order, or send an email. Each tool has different required arguments, and the agent must choose exactly one per step.

## Schema pattern

<CodeGroup>
  ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  {
    "anyOf": [
      {
        "type": "object",
        "properties": {
          "tool": { "const": "search_docs" },
          "query": { "type": "string", "minLength": 3, "maxLength": 200 },
          "top_k": { "type": "integer", "minimum": 1, "maximum": 10 }
        },
        "required": ["tool", "query", "top_k"],
        "additionalProperties": false
      },
      {
        "type": "object",
        "properties": {
          "tool": { "const": "lookup_order" },
          "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" }
        },
        "required": ["tool", "order_id"],
        "additionalProperties": false
      },
      {
        "type": "object",
        "properties": {
          "tool": { "const": "send_email" },
          "to": { "type": "string", "format": "email" },
          "subject": { "type": "string", "minLength": 3, "maxLength": 120 },
          "body": { "type": "string", "minLength": 10, "maxLength": 1000 }
        },
        "required": ["tool", "to", "subject", "body"],
        "additionalProperties": false
      }
    ]
  }
  ```

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

  class SearchDocs(BaseModel):
      model_config = ConfigDict(extra="forbid")
      tool: Literal["search_docs"]
      query: str = Field(..., min_length=3, max_length=200)
      top_k: int = Field(..., ge=1, le=10)

  class LookupOrder(BaseModel):
      model_config = ConfigDict(extra="forbid")
      tool: Literal["lookup_order"]
      order_id: str = Field(..., pattern=r"^ORD-[0-9]{4,10}$")

  class SendEmail(BaseModel):
      model_config = ConfigDict(extra="forbid")
      tool: Literal["send_email"]
      to: EmailStr
      subject: str = Field(..., min_length=3, max_length=120)
      body: str = Field(..., min_length=10, max_length=1000)

  ToolCall = Annotated[SearchDocs | LookupOrder | SendEmail, 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_docs"),
      query: z.string().min(3).max(200),
      top_k: z.number().int().min(1).max(10),
    }).strict(),
    z.object({
      tool: z.literal("lookup_order"),
      order_id: z.string().regex(/^ORD-[0-9]{4,10}$/),
    }).strict(),
    z.object({
      tool: z.literal("send_email"),
      to: z.string().email(),
      subject: z.string().min(3).max(120),
      body: z.string().min(10).max(1000),
    }).strict(),
  ]);
  ```

  ```bash curl theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  curl https://api.dottxt.ai/v1/chat/completions \
    -H "Authorization: Bearer $DOTTXT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "openai/gpt-oss-20b",
      "messages": [{ "role": "user", "content": "Retrieve the details of order ORD-9842." }],
      "response_format": {
        "type": "json_schema",
        "json_schema": {
          "name": "tool_call",
          "schema": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "tool": { "const": "search_docs" },
                  "query": { "type": "string", "minLength": 3, "maxLength": 200 },
                  "top_k": { "type": "integer", "minimum": 1, "maximum": 10 }
                },
                "required": ["tool", "query", "top_k"],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "tool": { "const": "lookup_order" },
                  "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" }
                },
                "required": ["tool", "order_id"],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "tool": { "const": "send_email" },
                  "to": { "type": "string", "format": "email" },
                  "subject": { "type": "string", "minLength": 3, "maxLength": 120 },
                  "body": { "type": "string", "minLength": 10, "maxLength": 1000 }
                },
                "required": ["tool", "to", "subject", "body"],
                "additionalProperties": false
              }
            ]
          }
        }
      }
    }'
  ```
</CodeGroup>

## Example outputs

Order lookup:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "tool": "lookup_order",
  "order_id": "ORD-9842"
}
```

Doc search:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "tool": "search_docs",
  "query": "password reset link expired",
  "top_k": 5
}
```

## Why this works

In this pattern, `anyOf` is enough because the discriminator makes the branches mutually exclusive. If the model tried to produce fields from two branches simultaneously, validation would still fail, so it learns to commit to one action.

The `const` value on `tool` acts as a discriminator: your runtime reads `tool`, matches it to a handler, and knows exactly which fields are present. No need for `if "query" in output` heuristics.

Each branch also has its own constraints (`pattern` on `order_id`, `format: "email"` on `to`, bounds on `subject` and `body`), so the arguments are validated at generation time, not at execution time.

## Related docs

* [Composition reference](/json-schema/reference/composition)
* [String reference](/json-schema/reference/string)
* [Agent Output cookbook](/json-schema/agent-output)
