# Authentication Source: https://docs.dottxt.ai/api/authentication The dottxt API uses bearer-token authentication. ## Header format ```http theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Authorization: Bearer ``` ## Environment variable ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} export DOTTXT_API_KEY="sk-dottxt-..." ``` ## Quick connection check ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} curl https://api.dottxt.ai/v1/models \ -H "Authorization: Bearer $DOTTXT_API_KEY" ``` If the key is valid, the API returns a `200` response with a `data` array of accessible models. ## Next steps * [List models](/api/list-models) * [Create chat completion](/api/chat-completions) # Create Chat Completion Source: https://docs.dottxt.ai/api/chat-completions ../openapi/dottxt-openapi.json POST /chat/completions Generate a model response from a message array, with support for token streaming, JSON Patch streaming, and tool calling. Use this endpoint for real-time chat generations on the OpenAI-compatible API. This documentation covers the OpenAI-compatible `chat/completions` endpoint. If an SDK defaults to the newer OpenAI Responses API, configure it to use chat completions instead. ## Base URL `https://api.dottxt.ai/v1` ## Structured output Use `response_format` with `type: "json_schema"` to constrain the model output to your schema. ```bash 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": "Classify: My card was charged twice for order ORD-9842. Need refund today." } ], "response_format": { "type": "json_schema", "json_schema": { "name": "ticket", "schema": { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "technical", "account", "shipping"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "summary": { "type": "string", "minLength": 10, "maxLength": 120 }, "tags": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 4 } }, "required": ["category", "priority", "summary", "tags"], "additionalProperties": false } } } }' ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "{\"category\": \"billing\", \"priority\": \"high\", \"summary\": \"Customer reports duplicate card charge on order ORD-9842, requesting refund\", \"tags\": [\"refund\", \"duplicate-charge\"]}" } } ] } ``` `category` is always one of the four enum values. `summary` is between 10 and 120 characters. `tags` has 1–4 items. See the [supported features](/supported-features) for the full list of enforceable constraints. ## JSON Patch streaming Set `stream: "patch"` alongside a `response_format` JSON schema to stream the structured response field-by-field as JSON Patch operations instead of returning a single completion. The server emits one RFC 6902 `add` operation per field as the model generates it, so downstream work (routing, dispatching, UI updates) can begin the moment the relevant field lands. ```bash 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": "I was charged twice this month. Refund the duplicate." } ], "stream": "patch", "response_format": { "type": "json_schema", "json_schema": { "name": "ticket", "schema": { "type": "object", "properties": { "intent": { "type": "string", "enum": ["billing", "technical", "account"] }, "urgency": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "reply": { "type": "string", "maxLength": 400 } }, "required": ["intent", "urgency", "reply"], "additionalProperties": false } } } }' ``` `stream: "patch"` is the only request difference from a normal structured-output call — `messages`, `temperature`, `max_tokens`, `seed`, and the rest of the chat-completions parameters all behave the same way. ### Wire format The default response framing is **NDJSON** (`Content-Type: application/x-ndjson`): one JSON object per line, no `event:` prefix, no trailing terminator record — the stream ends when the connection closes. ```ndjson Response (NDJSON) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"op":"add","path":"","value":{}} {"op":"add","path":"/intent","value":"billing"} {"op":"add","path":"/urgency","value":"high"} {"op":"add","path":"/reply","value":"Hi Jane, I've processed the refund..."} ``` The endpoint also speaks **Server-Sent Events** for clients that prefer SSE framing. Send `Accept: text/event-stream` on the request, and you'll get: ```sse Response (SSE) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} event: patch data: {"op":"add","path":"","value":{}} event: patch data: {"op":"add","path":"/intent","value":"billing"} event: patch data: {"op":"add","path":"/urgency","value":"high"} event: patch data: {"op":"add","path":"/reply","value":"Hi Jane, I've processed the refund..."} event: done data: {} ``` The JSON Patch payloads are identical between the two framings — only the transport differs. SSE adds a final `event: done` record before the connection closes; NDJSON closes silently. ### Operation shape Every record is an RFC 6902 `add` operation: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "op": "add", "path": "", "value": "" } ``` * **`op`** — always `"add"` in this mode. * **`path`** — JSON Pointer to the location being filled in. The root document arrives first as `path: ""` with `value: {}` (or `value: []` for array-rooted schemas). * **`value`** — the value being inserted. For leaf fields, the JSON primitive (string, number, boolean, null). For nested objects and arrays, an empty container that subsequent ops will fill. ### Order of operations Operations arrive in schema order: 1. **Root seed** — `{"op":"add","path":"","value":{}}` opens the document. For array-rooted schemas, `value` is `[]`. 2. **Leaf adds in schema order** — top-level scalar fields like `intent`, `urgency`, `reply`. 3. **Container seeds + items** — when the schema contains nested objects or arrays, the container is seeded first with `{}` or `[]`, then each item arrives as a separate add (`/steps/0`, `/steps/1`, ...). Nested objects work the same way (`/address`, then `/address/city`, `/address/zip`). A field's position in the schema determines when it streams. Design the schema so high-priority fields (routing keys, classifications, gates) come first; long-form fields (replies, explanations) come last. Example for a `{intent, urgency, steps: [...], reply}` schema: ```ndjson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"op":"add","path":"","value":{}} {"op":"add","path":"/intent","value":"billing"} {"op":"add","path":"/urgency","value":"high"} {"op":"add","path":"/steps","value":[]} {"op":"add","path":"/steps/0","value":"verify charge"} {"op":"add","path":"/steps/1","value":"issue refund"} {"op":"add","path":"/reply","value":"Hi Jane, I've processed the refund..."} ``` ### Reconstructing the document Each op is applied to the document state from the previous op. If you collect every op and apply them in order, you end up with the same JSON object a non-streaming request would have returned. The Python SDK exposes the running snapshot directly via `event.snapshot`; see [JSON Patch Streaming](/json-schema/streaming) for details. ### Patch streaming errors * The stream opens with `200 OK` once the model starts emitting. Validation errors (bad schema, malformed request, auth failure) come back as the standard JSON error response before any patch records are sent. * A non-200 status means no patch records will arrive; read the body for the error payload. * Connection failures mid-stream surface as a closed stream without a terminator. The Python SDK turns these into `dottxt.PatchStreamError`. ## Plain chat ```bash 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": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize why batch processing is useful." } ], "temperature": 0.3, "max_tokens": 180 }' ``` ## Example response shape ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1703187200, "model": "openai/gpt-oss-20b", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "Batch processing reduces cost and improves throughput for non-urgent workloads." } } ], "usage": { "prompt_tokens": 24, "completion_tokens": 36, "total_tokens": 60 } } ``` ## Notes * Set `stream: true` to receive token deltas as server-sent events; set `stream: "patch"` for [JSON Patch streaming](#json-patch-streaming). * For the Python SDK helper that consumes patch streams into `PatchEvent` objects, see [JSON Patch Streaming](/json-schema/streaming). * For model discovery, call [`GET /models`](/api/list-models). * For auth setup, see [Authentication](/api/authentication). * For failures, inspect the HTTP status and the `error` object in the response body. # List Models Source: https://docs.dottxt.ai/api/list-models ../openapi/dottxt-openapi.json GET /models Return the list of models available to your API key. Use this endpoint to discover exactly which model IDs your key can access before sending chat or embeddings requests. ## Base URL `https://api.dottxt.ai/v1` ## Practical request pattern ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} curl https://api.dottxt.ai/v1/models \ -H "Authorization: Bearer $DOTTXT_API_KEY" ``` ## Example response shape ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "object": "list", "data": [ { "id": "openai/gpt-oss-20b", "object": "model" } ] } ``` ## Notes * Use `data[].id` directly in `model` for [`POST /chat/completions`](/api/chat-completions). * If auth is missing or invalid, this endpoint returns `401`. # Models Catalog Source: https://docs.dottxt.ai/api/models Models available on the dottxt platform. ### Text | Model | Parameters | Use case | | ------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------- | | [`Qwen/Qwen3.5-397B-A17B`](https://huggingface.co/Qwen/Qwen3.5-397B-A17B) | 397B (17B active) | Frontier-level structured output. Best accuracy for complex schemas. | | [`Qwen/Qwen3-14B-FP8`](https://huggingface.co/Qwen/Qwen3-14B-FP8) | 14B | High-volume tasks and standard classification. | | [`openai/gpt-oss-20b`](https://huggingface.co/openai/gpt-oss-20b) | 21B (3.6B active) | Lower latency. Good for local or specialized use cases. | ### Vision | Model | Parameters | Use case | | ----------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------- | | [`Qwen/Qwen3-VL-235B-A22B-Instruct-FP8`](https://huggingface.co/Qwen/Qwen3-VL-235B-A22B-Instruct-FP8) | 235B (22B active) | Advanced multimodal model. Performance similar to GPT-5 Chat. | | [`Qwen/Qwen3-VL-30B-A3B-Instruct-FP8`](https://huggingface.co/Qwen/Qwen3-VL-30B-A3B-Instruct-FP8) | 30B (3B active) | Mid-size multimodal model. Performance similar to GPT-4.1-mini. | ## Next steps * [Create chat completion](/api/chat-completions) # API Overview Source: https://docs.dottxt.ai/api/overview OpenAI-compatible API basics: base URL, headers, auth, models, and endpoint map. The API follows the OpenAI chat completions format. If you have existing code that calls OpenAI, you can point it at dottxt by changing `base_url` and `api_key`. See the [migration guide](/migrate-from-other-providers) for details. This documentation covers the OpenAI-compatible `chat/completions` API surface. If an SDK defaults to the newer OpenAI Responses API, configure it to use chat completions instead. ## Base URL `https://api.dottxt.ai/v1` ## Required headers ```http theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Authorization: Bearer Content-Type: application/json ``` ## Structured output request Pass your JSON Schema in `response_format` ```bash 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": "Extract: John Smith , VP Engineering" } ], "response_format": { "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "role": { "type": "string" } }, "required": ["name", "email"], "additionalProperties": false } } } }' ``` The schema is compiled and enforced: ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1703187200, "model": "openai/gpt-oss-20b", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "{\"name\": \"John Smith\", \"email\": \"john@acme.com\", \"role\": \"VP Engineering\"}" } } ] } ``` `name` is guaranteed non-empty via `minLength`. `email` matches the pattern. `role` is not in `required`, so the model may omit it entirely when the input doesn't contain a job title. See endpoint-specific details in: * [Create Chat Completion](/api/chat-completions) * [List Models](/api/list-models) For migration and integration workflows, see [Integrations overview](/integrations/overview). ## Inference partners Our generation technology runs on top of a production inference stack operated by our launch partner, Doubleword. Doubleword Doubleword # Schema Review Source: https://docs.dottxt.ai/audit We offer a limited number of **complimentary schema audits** each month for companies with existing structured-output workflows. Submit your schema and examples, and we return a prioritized fix list. Submit your schema and examples to get a prioritized fix list. ## You can benefit from a schema review if * You already use JSON Schema, Pydantic, or Zod for outputs. * You are seeing invalid outputs, retries, or fragile post-processing logic. * You can share real schema files and representative examples. * A technical owner can implement changes in the next 1-2 weeks. ## Example finding Consider the following schema: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "status": { "type": "string" }, "summary": { "type": "string" } }, "required": ["status", "summary"] } ``` The model can return any string for `status` (`"open"`, `"Open"`, `"it's open"`, `"OPEN!!!"`), breaking downstream routing logic. After: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "status": { "type": "string", "enum": ["open", "in_progress", "resolved", "closed"] }, "summary": { "type": "string" } }, "required": ["status", "summary"] } ``` `status` is now constrained to known values. ## What we review * Schema correctness and constraint quality * Common failure points (missing constraints, ambiguity, weak field definitions) * Prompt-to-schema alignment ## What you get * A prioritized list of reliability issues * Concrete remediation steps with implementation guidance * A response within **48 hours** on business days ## Prepare before submitting * Primary schema file(s) (`*.schema.json`, Pydantic model, or Zod model) * One or two real examples that currently fail, including the model and parameters used * Any current prompt/task file if available * Success criteria (for example: lower retries, fewer invalid fields, higher field accuracy) Want to explore fixes on your own first? Start with [these tips](/json-schema/improve-your-schema). Submit your schema and examples to get a prioritized fix list. # Start Here Source: https://docs.dottxt.ai/index Pick the fastest path based on what you need right now. ## Start here Recommended first step. Run your first structured-output workflow in minutes. Keep your OpenAI-style calls and switch with minimal code changes. ## Explore by goal OpenAI-compatible endpoints, auth, and request formats. Learn schema patterns, references, and domain examples. Compare providers and see where schema support differs. Share your schema and get feedback before shipping. # Instructor Source: https://docs.dottxt.ai/integrations/instructor [Instructor](https://python.useinstructor.com/) patches the OpenAI client to return typed Pydantic objects instead of raw completions. Since dottxt exposes an OpenAI-compatible endpoint, you can use Instructor on top of the OpenAI Python SDK. For dottxt, the important detail is to use Instructor in JSON mode so your `response_model` is translated into JSON Schema and sent through dottxt structured generation, rather than relying on tool-calling behavior. Instructor has [excellent documentation](https://python.useinstructor.com/) covering advanced patterns like validation, retries, partial streaming, and multi-modal extraction. This page covers the dottxt-specific setup. Refer to the Instructor docs for everything else. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install instructor openai pydantic ``` ## Configure Create an OpenAI client pointed at dottxt, then patch it with Instructor in JSON mode: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import os import instructor from openai import OpenAI client = instructor.from_openai( OpenAI( base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ), mode=instructor.Mode.JSON, ) ``` ## Basic usage Define a Pydantic model and pass it as `response_model`. Instructor will derive JSON Schema from the model, send that schema to dottxt, and validate the response back into a Pydantic object: ```python Instructor theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Optional from pydantic import BaseModel, ConfigDict, Field class Contact(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(description="Full name") email: str = Field(description="Email address") role: Optional[str] = Field(default=None, description="Job title") contact = client.chat.completions.create( model="openai/gpt-oss-20b", response_model=Contact, messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], ) print(contact.name) # "John Smith" print(contact.email) # "john@acme.com" print(contact.role) # "VP Engineering" ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "name": "John Smith", "email": "john@acme.com", "role": "VP Engineering" } ``` Instructor handles schema generation, request construction, and response parsing for you. The underlying API call still uses the same dottxt structured generation path described in [API Overview](/api/overview) and [Pydantic Authoring](/json-schema/authoring/pydantic). ## What Instructor sends to dottxt Under the hood, the `Contact` model above is converted into JSON Schema and sent to dottxt as structured output constraints: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string", "description": "Full name" }, "email": { "type": "string", "description": "Email address" }, "role": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "Job title" } }, "required": ["name", "email"], "additionalProperties": false } ``` ## Nested models and enums ```python Instructor theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field class Tag(BaseModel): model_config = ConfigDict(extra="forbid") name: str confidence: float = Field(ge=0.0, le=1.0) class TicketExtraction(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(description="Short summary of the issue") priority: Literal["low", "medium", "high", "critical"] tags: list[Tag] assignee: Optional[str] = None ticket = client.chat.completions.create( model="openai/gpt-oss-20b", response_model=TicketExtraction, messages=[ { "role": "user", "content": ( "Parse this support ticket: " "URGENT: Payment gateway returning 500 errors on checkout. " "Tags: payments, backend, production-incident. " "Assign to the payments team." ), } ], ) ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "title": "Payment gateway 500 errors on checkout", "priority": "critical", "tags": [ {"name": "payments", "confidence": 0.95}, {"name": "backend", "confidence": 0.9}, {"name": "production-incident", "confidence": 0.95} ], "assignee": "payments team" } ``` ## Streaming partial results Use `create_partial` to yield progressively-complete model instances as tokens stream in: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Optional from pydantic import BaseModel, ConfigDict, Field class Contact(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(description="Full name") email: str = Field(description="Email address") role: Optional[str] = Field(default=None, description="Job title") for partial in client.chat.completions.create_partial( model="openai/gpt-oss-20b", response_model=Contact, messages=[ {"role": "user", "content": "Extract: Alice Chen , CTO"} ], ): print(partial) ``` ## Notes * Use `mode=instructor.Mode.JSON` with dottxt so Instructor goes through the structured output path instead of defaulting to tool calling. * `ConfigDict(extra="forbid")` is useful when you want `additionalProperties: false` in the generated schema. * `create_with_completion()` returns both the parsed model and the raw completion, useful for inspecting token usage. * See the [Pydantic authoring guide](/json-schema/authoring/pydantic) for how to write effective schemas. # LangGraph Source: https://docs.dottxt.ai/integrations/langgraph [LangGraph](https://langchain-ai.github.io/langgraph/) is a graph-based agent framework by the LangChain team. It uses `ChatOpenAI` for model calls, which supports custom OpenAI-compatible endpoints like dottxt. For dottxt, the key integration point is LangChain's structured-output support: bind a schema to `ChatOpenAI`, and LangChain will send the corresponding structured output request to dottxt and parse the result back into a typed object. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install langgraph langchain-openai pydantic ``` ## Configure Create a `ChatOpenAI` instance pointed at dottxt: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="openai/gpt-oss-20b", base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ) ``` ## Structured output Use `with_structured_output()` to bind a Pydantic model to the LLM. The result is a typed object: ```python LangGraph theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Sentiment(BaseModel): model_config = ConfigDict(extra="forbid") label: str = Field(description="positive, negative, or neutral") confidence: float = Field(ge=0.0, le=1.0) reasoning: str structured_llm = llm.with_structured_output( Sentiment, method="json_schema", ) result = structured_llm.invoke("Analyze the sentiment: 'This product is excellent!'") print(result.label) # "positive" print(result.confidence) # 0.95 ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "label": "positive", "confidence": 0.95, "reasoning": "The word 'excellent' is strongly positive." } ``` LangChain builds the structured output request and parses the JSON response back into your Pydantic model. Under the hood, this still uses the same dottxt structured generation flow described in [API Overview](/api/overview). ## Using in a graph Combine structured output with LangGraph's `StateGraph` for multi-step workflows: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing_extensions import TypedDict from typing import Literal from pydantic import BaseModel, ConfigDict, Field from langgraph.graph import StateGraph, START, END class Classification(BaseModel): model_config = ConfigDict(extra="forbid") category: Literal["billing", "account", "bug", "feature"] priority: Literal["low", "medium", "high"] summary: str = Field(min_length=10, max_length=120) class State(TypedDict): text: str result: Classification | None structured_llm = llm.with_structured_output( Classification, method="json_schema", ) def classify(state: State) -> dict: return {"result": structured_llm.invoke( f"Classify this support ticket: {state['text']}" )} graph = StateGraph(State) graph.add_node("classify", classify) graph.add_edge(START, "classify") graph.add_edge("classify", END) app = graph.compile() output = app.invoke({"text": "I can't log in to my account", "result": None}) print(output["result"].category) print(output["result"].priority) ``` ## Notes * Prefer `method="json_schema"` with dottxt so LangChain uses the structured output path explicitly. * Graph nodes are plain functions that receive the full state and return a partial dict of updates. * `ConfigDict(extra="forbid")` is useful when you want `additionalProperties: false` in the generated schema. * See the [Pydantic authoring guide](/json-schema/authoring/pydantic) for how to write effective schemas. # Integrations Overview Source: https://docs.dottxt.ai/integrations/overview Choose a migration path, then use framework integration guides. Use this section for migration and integration patterns. ## Start here * [Migrate from other providers](/migrate-from-other-providers) * [Quickstart](/quickstart) * [API overview](/api/overview) ## Framework integrations Use these when you need framework-native orchestration: * [Instructor](/integrations/instructor) * [Vercel AI SDK](/integrations/vercel-ai-sdk) * [LangGraph](/integrations/langgraph) * [Pydantic AI](/integrations/pydantic-ai) ## Schema authoring Schema authoring guides now live under JSON Schema: * [Pydantic Authoring](/json-schema/authoring/pydantic) * [Zod Authoring](/json-schema/authoring/zod) * [TypeBox Authoring](/json-schema/authoring/typebox) # Pydantic AI Source: https://docs.dottxt.ai/integrations/pydantic-ai [Pydantic AI](https://ai.pydantic.dev/) is the official agent framework by the Pydantic team. It supports typed structured output via `output_type` and works with any OpenAI-compatible endpoint. For dottxt, `output_type` is the important integration point: Pydantic AI derives JSON Schema from your output model, sends that schema through the OpenAI-compatible API, and parses the result back into a typed object. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install pydantic-ai openai pydantic ``` ## Configure Create an `OpenAIProvider` pointed at dottxt, then wrap it in an `OpenAIChatModel`: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import os from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider provider = OpenAIProvider( base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ) model = OpenAIChatModel("openai/gpt-oss-20b", provider=provider) ``` ## Basic usage Pass a Pydantic model as `output_type` to get typed structured output: ```python Pydantic AI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field from pydantic_ai import Agent class CityLocation(BaseModel): model_config = ConfigDict(extra="forbid") city: str = Field(description="City name") country: str = Field(description="Country name") agent = Agent(model, output_type=CityLocation) result = agent.run_sync("Where were the 2012 Olympics held?") print(result.output) # CityLocation(city='London', country='United Kingdom') ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "city": "London", "country": "United Kingdom" } ``` Pydantic AI generates the schema from `output_type` and parses the response back into `result.output`. Under the hood, this still uses the same dottxt structured generation flow described in [API Overview](/api/overview) and [Pydantic Authoring](/json-schema/authoring/pydantic). ## Agent with dependencies and tools Use `deps_type` to inject runtime context, and `@agent.tool` to give the agent callable functions: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from dataclasses import dataclass from typing import Literal from pydantic import BaseModel, ConfigDict, Field from pydantic_ai import Agent, RunContext @dataclass class SupportDeps: customer_name: str account_id: int class SupportResponse(BaseModel): model_config = ConfigDict(extra="forbid") greeting: str answer: str follow_up_question: str = Field(min_length=8) priority: Literal["low", "medium", "high"] agent = Agent( model, output_type=SupportResponse, deps_type=SupportDeps, instructions="You are a helpful customer support agent. Always greet the customer by name.", ) @agent.tool def get_account_status(ctx: RunContext[SupportDeps]) -> str: """Look up the account status for the current customer.""" return f"Account #{ctx.deps.account_id} is active and in good standing." result = agent.run_sync( "What is the status of my account?", deps=SupportDeps(customer_name="Alice", account_id=42), ) print(result.output.greeting) print(result.output.answer) ``` ## Notes * Use `output_type`, not `result_type`; the latter was removed in Pydantic AI v0.6.0. * `agent.run()` is async, `agent.run_sync()` is synchronous, `agent.run_stream()` is async streaming. * The result is accessed via `result.output`, typed according to `output_type`. * `ConfigDict(extra="forbid")` is useful when you want `additionalProperties: false` in the generated schema. * See the [Pydantic authoring guide](/json-schema/authoring/pydantic) for how to write effective schemas. # Vercel AI SDK Source: https://docs.dottxt.ai/integrations/vercel-ai-sdk The [Vercel AI SDK](https://ai-sdk.dev/) provides `generateObject` and `streamObject` for structured output in TypeScript. Since dottxt is OpenAI-compatible, use `@ai-sdk/openai` with a custom `baseURL`. For dottxt, `generateObject` and `streamObject` are the key integration points: you provide a Zod or JSON Schema schema, the SDK sends a structured output request to dottxt, and the response is parsed back into a typed object. Use `dottxt.chat(...)` for these examples so the SDK targets the OpenAI-compatible chat completions API rather than the Responses API. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} npm install ai @ai-sdk/openai zod ``` ## Configure Create a provider pointed at dottxt: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { createOpenAI } from "@ai-sdk/openai"; const dottxt = createOpenAI({ baseURL: "https://api.dottxt.ai/v1", apiKey: process.env.DOTTXT_API_KEY!, }); ``` ## Basic usage with Zod Pass a Zod schema to `generateObject` for typed structured output: ```typescript Vercel AI SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { generateObject } from "ai"; import { z } from "zod"; const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: z.object({ name: z.string().min(1).describe("Full name"), email: z.string().describe("Email address"), role: z.string().describe("Job title").optional(), }), prompt: "Extract: John Smith , VP Engineering", }); console.log(object.name); // "John Smith" console.log(object.email); // "john@acme.com" ``` ```json Response theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "name": "John Smith", "email": "john@acme.com", "role": "VP Engineering" } ``` The AI SDK builds the structured output request and validates the result against your schema. Under the hood, this still uses the same dottxt structured generation flow described in [API Overview](/api/overview). ## Using raw JSON Schema Use `jsonSchema()` when you have a JSON Schema object instead of a Zod schema: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { generateObject, jsonSchema } from "ai"; const contactSchema = jsonSchema<{ name: string; email: string; role: string; }>({ type: "object", properties: { name: { type: "string" }, email: { type: "string" }, role: { type: "string" }, }, required: ["name", "email", "role"], additionalProperties: false, }); const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: contactSchema, schemaName: "contact", prompt: "Extract: John Smith , VP Engineering", }); ``` This is useful when integrating with [TypeBox](/json-schema/authoring/typebox) or other schema libraries that produce raw JSON Schema objects. ## Streaming Use `streamObject` to receive partial results as tokens stream in: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { streamObject } from "ai"; import { z } from "zod"; const { partialObjectStream } = streamObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: z.object({ title: z.string(), summary: z.string(), tags: z.array(z.string()).max(5), }), prompt: "Summarize: structured output improves LLM reliability...", }); for await (const partial of partialObjectStream) { console.log(partial); } ``` ## Notes * `generateObject` returns a fully validated object. `streamObject` yields partial objects as they stream. * Use `dottxt.chat(...)` instead of `dottxt(...)` with dottxt's current OpenAI-compatible API support. The default model helper uses the Responses API path. * The `schemaName` parameter is optional but recommended when using `jsonSchema()`; it sets the `name` field in the request for better model guidance. * Use schema constraints like `.min()`, `.max()`, enums, and `.describe()` to improve output quality and make the generated schema more specific. * See the [Zod authoring guide](/json-schema/authoring/zod) for how to write effective schemas. # Additional Properties Source: https://docs.dottxt.ai/json-schema/additional-properties Control schema strictness by deciding where unknown keys are allowed. `additionalProperties` controls whether fields not listed in `properties` are allowed. It can be set to either `true` or `false`. Setting it to `false` locks the schema to exactly the declared fields: the model cannot invent extra keys, and your application knows precisely what to expect. This is one of the highest-leverage settings in production schemas. Without it, the model might add a helpful-looking `"notes"` field that no consumer knows how to handle, or a `"timestamp"` that conflicts with your own timestamping logic. These extra fields pass validation silently and cause bugs downstream. The nuance is that strict everywhere isn't always right. Sometimes you need a flexible container, such as a `metadata` object for vendor-specific keys or an `extras` bag for forward compatibility. The pattern is: strict at the top level, flexible in one designated location. ## Use case Audit logs that need a strict top-level structure for indexing and compliance, but a `metadata` object that can carry vendor-specific keys without schema changes. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "event_id": { "type": "string" }, "event_type": { "type": "string", "enum": ["login", "logout", "password_reset"] }, "actor": { "type": "object", "properties": { "user_id": { "type": "string" }, "ip": { "type": "string" } }, "required": ["user_id", "ip"], "additionalProperties": false }, "metadata": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean", "null"] } } }, "required": ["event_id", "event_type", "actor", "metadata"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class Actor(BaseModel): model_config = ConfigDict(extra="forbid") user_id: str ip: str class AuditEvent(BaseModel): model_config = ConfigDict(extra="forbid") event_id: str event_type: Literal["login", "logout", "password_reset"] actor: Actor metadata: dict[str, str | float | bool | None] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const actorSchema = z.object({ user_id: z.string(), ip: z.string(), }).strict(); const auditEventSchema = z.object({ event_id: z.string(), event_type: z.enum(["login", "logout", "password_reset"]), actor: actorSchema, metadata: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])), }).strict(); ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "event_id": "evt_6f1a", "event_type": "login", "actor": { "user_id": "usr_331", "ip": "203.0.113.10" }, "metadata": { "device": "ios", "mfa": true, "attempt": 1 } } ``` ## Why this works The top-level object and the `actor` sub-object both have `additionalProperties: false`, so no unexpected fields can appear in the core structure. Your indexing pipeline knows exactly which fields exist and can map them directly to database columns or search indices. The `metadata` object uses `additionalProperties: { "type": ["string", "number", "boolean", "null"] }`; it allows arbitrary keys, but constrains their values to primitive types. This prevents deeply nested or complex structures from sneaking into what should be a flat key-value bag. New metadata keys appear without schema changes, but they can't break your storage layer. ## Related docs * [Object reference](/json-schema/reference/object) * [Improve Your Schema](/json-schema/improve-your-schema) # Agent Output Source: https://docs.dottxt.ai/json-schema/agent-output Design strict agent envelopes so orchestration code can execute tool calls without heuristics. Agent loops typically alternate between "call a tool" and "return a final answer." This pattern is for manual tool handling: the model emits a structured `tool_call` envelope, and your application is responsible for executing the tool and continuing the loop. That is different from OpenAI-style built-in tool calling, where the API returns tool call objects in its own response format. Without a strict output contract, the model might mix fields from both branches, invent new action types, or omit the information your executor needs to dispatch. A `oneOf` schema with a `const` discriminator eliminates this: the model picks exactly one branch, emits exactly the right fields, and your orchestration code can dispatch without parsing heuristics. ## Use case You run an agent that can either call a tool or return a final answer. You want one schema that covers both outcomes, with a confidence score on every action for routing and observability. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "kind": { "const": "tool_call" }, "tool": { "type": "string", "enum": ["search_docs", "lookup_order", "send_email"] }, "arguments": { "type": "object", "additionalProperties": true }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["kind", "tool", "arguments", "confidence"], "additionalProperties": false }, { "type": "object", "properties": { "kind": { "const": "final_answer" }, "answer": { "type": "string", "minLength": 1, "maxLength": 1200 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["kind", "answer", "confidence"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Any, Literal from pydantic import BaseModel, ConfigDict, Field class ToolCall(BaseModel): model_config = ConfigDict(extra="forbid") kind: Literal["tool_call"] tool: Literal["search_docs", "lookup_order", "send_email"] arguments: dict[str, Any] confidence: float = Field(..., ge=0, le=1) class FinalAnswer(BaseModel): model_config = ConfigDict(extra="forbid") kind: Literal["final_answer"] answer: str = Field(..., min_length=1, max_length=1200) confidence: float = Field(..., ge=0, le=1) AgentOutput = Annotated[ToolCall | FinalAnswer, Field(discriminator="kind")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const agentOutputSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("tool_call"), tool: z.enum(["search_docs", "lookup_order", "send_email"]), arguments: z.record(z.string(), z.any()), confidence: z.number().min(0).max(1), }).strict(), z.object({ kind: z.literal("final_answer"), answer: z.string().min(1).max(1200), confidence: z.number().min(0).max(1), }).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": "Use the lookup_order tool to check order ORD-9842." }], "response_format": { "type": "json_schema", "json_schema": { "name": "agent_output", "schema": { "oneOf": [ { "type": "object", "properties": { "kind": { "const": "tool_call" }, "tool": { "type": "string", "enum": ["search_docs", "lookup_order", "send_email"] }, "arguments": { "type": "object", "additionalProperties": true }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["kind", "tool", "arguments", "confidence"], "additionalProperties": false }, { "type": "object", "properties": { "kind": { "const": "final_answer" }, "answer": { "type": "string", "minLength": 1, "maxLength": 1200 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["kind", "answer", "confidence"], "additionalProperties": false } ] } } } }' ``` ## Prompt snippet ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Choose exactly one action. - If a tool is required, return kind="tool_call" with tool and arguments. - If no tool is required, return kind="final_answer" with answer. Never include fields from both branches. ``` ## Example outputs Tool call: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "kind": "tool_call", "tool": "lookup_order", "arguments": { "order_id": "ORD-9842" }, "confidence": 0.94 } ``` Final answer: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "kind": "final_answer", "answer": "Order ORD-9842 shipped on 2026-02-20 and is expected tomorrow.", "confidence": 0.88 } ``` ## Why this works `oneOf` enforces mutual exclusivity: the model must produce either a `tool_call` or a `final_answer`, never a hybrid. Your executor reads `kind`, dispatches to the right handler, and never needs to guess what the model intended. The `const` discriminator on `kind` removes ambiguity. Without it, both branches might structurally overlap (both could have a string field), and validation alone couldn't tell them apart. Confidence appears in both branches, so your orchestration layer can apply the same routing logic regardless of action type, for example, escalating to a human when confidence drops below a threshold. ## Related docs * [Composition reference](/json-schema/reference/composition) * [Object reference](/json-schema/reference/object) * [Union of Objects cookbook](/json-schema/union-of-objects) # Schema composition with anyOf Source: https://docs.dottxt.ai/json-schema/anyof-object-variants Use discriminated branches to validate one of several alternative shapes. Some fields only make sense in certain contexts. Requiring an email address when the user chose SMS delivery is noise; omitting a phone number when they chose SMS is a bug. A discriminated union encodes this cleanly: one branch for email, one branch for SMS. ## Use case A checkout flow where `delivery_method` decides which contact details are mandatory. When the user wants email delivery, you need their email address. When they want SMS, you need their phone number. Both should never be required simultaneously. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "anyOf": [ { "type": "object", "properties": { "delivery_method": { "type": "string", "const": "email" }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" } }, "required": ["delivery_method", "email"], "additionalProperties": false }, { "type": "object", "properties": { "delivery_method": { "type": "string", "const": "sms" }, "phone": { "type": "string", "pattern": "^\\+?[1-9][0-9]{7,14}$" } }, "required": ["delivery_method", "phone"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field class EmailDelivery(BaseModel): model_config = ConfigDict(extra="forbid") delivery_method: Literal["email"] email: str = Field(..., pattern=r"^[^@]+@[^@]+$") class SmsDelivery(BaseModel): model_config = ConfigDict(extra="forbid") delivery_method: Literal["sms"] phone: str = Field(..., pattern=r"^\+?[1-9][0-9]{7,14}$") Delivery = Annotated[EmailDelivery | SmsDelivery, Field(discriminator="delivery_method")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const deliverySchema = z.discriminatedUnion("delivery_method", [ z.object({ delivery_method: z.literal("email"), email: z.string().regex(/^[^@]+@[^@]+$/), }).strict(), z.object({ delivery_method: z.literal("sms"), phone: z.string().regex(/^\+?[1-9][0-9]{7,14}$/), }).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": "Send order confirmation to jane@acme.com via email." }], "response_format": { "type": "json_schema", "json_schema": { "name": "delivery_method", "schema": { "anyOf": [ { "type": "object", "properties": { "delivery_method": { "type": "string", "const": "email" }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" } }, "required": ["delivery_method", "email"], "additionalProperties": false }, { "type": "object", "properties": { "delivery_method": { "type": "string", "const": "sms" }, "phone": { "type": "string", "pattern": "^\\+?[1-9][0-9]{7,14}$" } }, "required": ["delivery_method", "phone"], "additionalProperties": false } ] } } } }' ``` ## Prompt snippet ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Infer the delivery method from the user request. If method is email, include email. If method is sms, include phone in international format. ``` ## Example outputs ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "delivery_method": "email", "email": "jane@acme.com" } ``` ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "delivery_method": "sms", "phone": "+14155551234" } ``` ## Why this works The `anyOf` branches split the payload into two exact shapes. The email branch requires `email`, and the SMS branch requires `phone`. Because `delivery_method` is fixed with `const` in each branch, the model cannot mix fields across branches, so `anyOf` is equivalent to `oneOf` here. ## Related docs * [Conditionals reference](/json-schema/reference/conditionals) * [String reference](/json-schema/reference/string) # API Calls Source: https://docs.dottxt.ai/json-schema/api-calls Generate execution-ready API call plans from natural language requests. Turning natural language into API calls is one of the most direct applications of structured output. The user says "refund \$25 for order ORD-9842," and the model produces a valid `POST /refunds` request with the right body fields. The schema defines every endpoint as a `oneOf` branch with `const` values for method and path, so the model can't invent endpoints that don't exist or mix parameters from different calls. This pattern is especially valuable for internal tools where users interact with backend APIs through a natural language interface. The schema acts as a whitelist of allowed operations. ## Goal Map natural language intent into exactly one valid API call definition, with validated parameters, ready for your backend to execute. ## Schema contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "endpoint": { "const": "get_order" }, "method": { "const": "GET" }, "path": { "const": "/orders/{order_id}" }, "path_params": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" } }, "required": ["order_id"], "additionalProperties": false } }, "required": ["endpoint", "method", "path", "path_params"], "additionalProperties": false }, { "type": "object", "properties": { "endpoint": { "const": "create_refund" }, "method": { "const": "POST" }, "path": { "const": "/refunds" }, "body": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" }, "amount": { "type": "number", "minimum": 0.01 }, "reason": { "type": "string", "enum": ["duplicate", "fraud", "requested_by_customer", "other"] } }, "required": ["order_id", "amount", "reason"], "additionalProperties": false } }, "required": ["endpoint", "method", "path", "body"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field class GetOrderPathParams(BaseModel): model_config = ConfigDict(extra="forbid") order_id: str = Field(..., pattern=r"^ORD-[0-9]{4,10}$") class GetOrderCall(BaseModel): model_config = ConfigDict(extra="forbid") endpoint: Literal["get_order"] method: Literal["GET"] path: Literal["/orders/{order_id}"] path_params: GetOrderPathParams class RefundBody(BaseModel): model_config = ConfigDict(extra="forbid") order_id: str = Field(..., pattern=r"^ORD-[0-9]{4,10}$") amount: float = Field(..., ge=0.01) reason: Literal["duplicate", "fraud", "requested_by_customer", "other"] class CreateRefundCall(BaseModel): model_config = ConfigDict(extra="forbid") endpoint: Literal["create_refund"] method: Literal["POST"] path: Literal["/refunds"] body: RefundBody ApiCall = Annotated[GetOrderCall | CreateRefundCall, Field(discriminator="endpoint")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const apiCallSchema = z.discriminatedUnion("endpoint", [ z.object({ endpoint: z.literal("get_order"), method: z.literal("GET"), path: z.literal("/orders/{order_id}"), path_params: z.object({ order_id: z.string().regex(/^ORD-[0-9]{4,10}$/), }).strict(), }).strict(), z.object({ endpoint: z.literal("create_refund"), method: z.literal("POST"), path: z.literal("/refunds"), body: z.object({ order_id: z.string().regex(/^ORD-[0-9]{4,10}$/), amount: z.number().min(0.01), reason: z.enum(["duplicate", "fraud", "requested_by_customer", "other"]), }).strict(), }).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": "Issue a $25 refund for order ORD-9842 because the customer was charged twice." }], "response_format": { "type": "json_schema", "json_schema": { "name": "api_call", "schema": { "oneOf": [ { "type": "object", "properties": { "endpoint": { "const": "get_order" }, "method": { "const": "GET" }, "path": { "const": "/orders/{order_id}" }, "path_params": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" } }, "required": ["order_id"], "additionalProperties": false } }, "required": ["endpoint", "method", "path", "path_params"], "additionalProperties": false }, { "type": "object", "properties": { "endpoint": { "const": "create_refund" }, "method": { "const": "POST" }, "path": { "const": "/refunds" }, "body": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD-[0-9]{4,10}$" }, "amount": { "type": "number", "minimum": 0.01 }, "reason": { "type": "string", "enum": ["duplicate", "fraud", "requested_by_customer", "other"] } }, "required": ["order_id", "amount", "reason"], "additionalProperties": false } }, "required": ["endpoint", "method", "path", "body"], "additionalProperties": false } ] } } } }' ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Issue a $25 refund for order ORD-9842 because the customer was charged twice. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "endpoint": "create_refund", "method": "POST", "path": "/refunds", "body": { "order_id": "ORD-9842", "amount": 25, "reason": "duplicate" } } ``` ## Implementation tips * **`const` on method and path locks each branch.** The model can't produce `"method": "DELETE"` for the refund endpoint or change the path. This makes the schema a whitelist of allowed operations. * **`oneOf` ensures a single action.** The model commits to one endpoint per request. If you need multi-step operations, run the model in a loop; don't try to batch multiple API calls into one schema. * **Parameter-level constraints catch bad input early.** The `order_id` pattern (`^ORD-[0-9]{4,10}$`) and `amount` minimum (`0.01`) reject invalid values at generation time, before your API sees them. * **Validate before executing.** Even with a strict schema, run the output through your API's own validation layer. The schema catches structural issues; your API validates business rules (e.g., "this order is not eligible for refund"). ## Related docs * [Unions of objects](/json-schema/union-of-objects): discriminated unions for routing to different endpoint shapes * [Agent output](/json-schema/agent-output): structure agent responses with tool calls and final answers * [Conditional Requirements](/json-schema/conditional-requirements): require different parameters depending on the endpoint * [Composition reference](/json-schema/reference/composition) | [Object reference](/json-schema/reference/object) # Pydantic Source: https://docs.dottxt.ai/json-schema/authoring/pydantic Author JSON Schema with Pydantic models and use it with dottxt structured generation. [Pydantic](https://docs.pydantic.dev/) is the most common way to author JSON Schemas in Python. You define a model class, Pydantic generates JSON Schema with `model_json_schema()`, and dottxt can enforce that schema through the OpenAI-compatible API. ## Use with dottxt Pydantic is a good fit when you want Python-native types, schemas defined as code, and runtime validation from the same model definitions. ### Install ```bash dottxt SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install dottxt ``` ```bash OpenAI SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install openai pydantic ``` ### Basic usage The dottxt SDK accepts a Pydantic model directly as `response_format` and returns a validated instance: ```python Pydantic + dottxt SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, Field from dottxt import DotTxt class IncidentSummary(BaseModel): severity: Literal["low", "medium", "high"] team: str = Field(max_length=32) client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input=( "Summarize this incident: checkout errors are blocking purchases. " "Return a JSON object with keys severity and team." ), response_format=IncidentSummary, ) print(result) # severity='high' team='checkout' print(result.model_dump()) # {'severity': 'high', 'team': 'checkout'} ``` If you'd rather call the OpenAI-compatible API directly, generate JSON Schema from the model with `model_json_schema()` and send it in `response_format`: ```python Pydantic + OpenAI SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import os from openai import OpenAI from pydantic import BaseModel, ConfigDict, Field class Contact(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(description="Full name") email: str = Field(description="Email address") role: str | None = Field(default=None, description="Job title") client = OpenAI( base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ) response = client.chat.completions.create( model="openai/gpt-oss-20b", messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": Contact.model_json_schema(), }, }, ) contact = Contact.model_validate_json(response.choices[0].message.content) print(contact.name) print(contact.email) print(contact.role) ``` ```json JSON Schema (sent to API) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string", "description": "Full name" }, "email": { "type": "string", "description": "Email address" }, "role": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "Job title" } }, "required": ["name", "email"], "additionalProperties": false } ``` ```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": "Extract: John Smith , VP Engineering" }], "response_format": { "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": { "type": "string", "description": "Full name" }, "email": { "type": "string", "description": "Email address" }, "role": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null, "description": "Job title" } }, "required": ["name", "email"], "additionalProperties": false } } } }' ``` Set `model_config = ConfigDict(extra="forbid")` when you want strict object schemas. Without it, the generated schema allows extra properties. Pydantic also generates a `title` for fields and models. Those are omitted from the examples below for readability. ### Add constraints and descriptions Use `Field()` for constraints and descriptions. Use `Literal` for enum values: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict, Field class SupportTicket(BaseModel): model_config = ConfigDict(extra="forbid") category: Literal["billing", "account", "bug", "feature"] = Field( description="The area this ticket relates to." ) priority: Literal["low", "medium", "high"] = Field( description="How urgently this ticket needs attention." ) summary: str = Field( min_length=10, max_length=500, description="A brief description of the issue." ) confidence: float = Field( ge=0.0, le=1.0, description="How confident the model is in the classification." ) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "account", "bug", "feature"], "description": "The area this ticket relates to." }, "priority": { "type": "string", "enum": ["low", "medium", "high"], "description": "How urgently this ticket needs attention." }, "summary": { "type": "string", "minLength": 10, "maxLength": 500, "description": "A brief description of the issue." }, "confidence": { "type": "number", "minimum": 0.0, "maximum": 1.0, "description": "How confident the model is in the classification." } }, "required": ["category", "priority", "summary", "confidence"], "additionalProperties": false } ``` Descriptions help guide generation, but they are not enforceable constraints like `enum`, `pattern`, or `required`. ## Reference Use the sections below as a reference for how common Pydantic patterns map to JSON Schema. ### Enums Use enums when a field must be one of a fixed set of known values. Use `Literal` when a field should be limited to a fixed set of values: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict, Field class Sentiment(BaseModel): model_config = ConfigDict(extra="forbid") label: Literal["positive", "negative", "neutral"] confidence: float = Field(ge=0.0, le=1.0) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "label": { "type": "string", "enum": ["positive", "negative", "neutral"] }, "confidence": { "type": "number", "minimum": 0.0, "maximum": 1.0 } }, "required": ["label", "confidence"], "additionalProperties": false } ``` Python's `enum.Enum` also works. Pydantic puts the enum definition in `$defs` and references it: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from enum import Enum from pydantic import BaseModel, ConfigDict class Color(str, Enum): red = "red" green = "green" blue = "blue" class Palette(BaseModel): model_config = ConfigDict(extra="forbid") primary: Color accent: Color ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$defs": { "Color": { "type": "string", "enum": ["red", "green", "blue"] } }, "type": "object", "properties": { "primary": { "$ref": "#/$defs/Color" }, "accent": { "$ref": "#/$defs/Color" } }, "required": ["primary", "accent"], "additionalProperties": false } ``` Prefer `Literal` when the values are only used once. Use `Enum` when you want to reuse the same set of values across fields or models. ### Const Use const-style fields when a value should never vary. Use a single-value `Literal[...]` when a field must always have one exact value: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class SearchStep(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal["search"] query: str ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "action": { "const": "search", "type": "string" }, "query": { "type": "string" } }, "required": ["action", "query"], "additionalProperties": false } ``` ### Optional and nullable fields Use optional and nullable fields carefully because they produce different schema contracts. ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Lead(BaseModel): model_config = ConfigDict(extra="forbid") name: str email: str company: str | None = None phone: str | None = None notes: str | None = Field(...) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "company": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null }, "phone": { "anyOf": [{ "type": "string" }, { "type": "null" }], "default": null }, "notes": { "anyOf": [{ "type": "string" }, { "type": "null" }] } }, "required": ["name", "email", "notes"], "additionalProperties": false } ``` Fields typed as `T | None` with a default of `None` become nullable and optional. Fields typed as `T | None = Field(...)` stay required but nullable. See [Optional vs Null](/json-schema/optional-vs-null) for the semantic difference. ### Arrays and lists Use list types for repeated values, then add bounds on the list or its items as needed. Use `list[T]` for array fields. Pydantic maps list `min_length` and `max_length` to `minItems` and `maxItems`: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Survey(BaseModel): model_config = ConfigDict(extra="forbid") question: str options: list[str] = Field(min_length=2, max_length=6) tags: list[str] = Field(default_factory=list, max_length=5) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "question": { "type": "string" }, "options": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 6 }, "tags": { "type": "array", "items": { "type": "string" }, "maxItems": 5 } }, "required": ["question", "options"], "additionalProperties": false } ``` Setting bounds on arrays prevents the model from generating unbounded lists. See [Bounded Arrays](/json-schema/bounded-arrays) for more. If you need constraints on each item, put them on the item type: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated from pydantic import Field Tag = Annotated[str, Field(min_length=1, max_length=40)] tags: list[Tag] = Field(max_length=5) ``` ### Formats and specialized types Use specialized Pydantic types when you want the generated schema to carry semantic format information. Use Pydantic's built-in types when you want semantic formats in the generated schema: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from datetime import date from pydantic import BaseModel, ConfigDict, EmailStr class ContactRecord(BaseModel): model_config = ConfigDict(extra="forbid") email: EmailStr signup_date: date ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "signup_date": { "type": "string", "format": "date" } }, "required": ["email", "signup_date"], "additionalProperties": false } ``` Prefer semantic types like `EmailStr` and `date` over plain `str` when you want the schema to carry format information. ### Nested models Use nested models to reuse object shapes and keep larger schemas maintainable. Nested models become `$defs` references in the generated schema: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict class Address(BaseModel): model_config = ConfigDict(extra="forbid") street: str city: str country: str class Customer(BaseModel): model_config = ConfigDict(extra="forbid") name: str billing_address: Address shipping_address: Address ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$defs": { "Address": { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "country": { "type": "string" } }, "required": ["street", "city", "country"], "additionalProperties": false } }, "type": "object", "properties": { "name": { "type": "string" }, "billing_address": { "$ref": "#/$defs/Address" }, "shipping_address": { "$ref": "#/$defs/Address" } }, "required": ["name", "billing_address", "shipping_address"], "additionalProperties": false } ``` ### Discriminated unions Use discriminated unions when the output can take one of several object shapes. Use `Literal` with `Field(discriminator=...)` to generate tagged `oneOf` schemas in Pydantic: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict, Field class SearchAction(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal["search"] query: str class LookupAction(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal["lookup"] id: int class AgentOutput(BaseModel): model_config = ConfigDict(extra="forbid") step: SearchAction | LookupAction = Field(discriminator="action") ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$defs": { "SearchAction": { "type": "object", "properties": { "action": { "const": "search", "type": "string" }, "query": { "type": "string" } }, "required": ["action", "query"], "additionalProperties": false }, "LookupAction": { "type": "object", "properties": { "action": { "const": "lookup", "type": "string" }, "id": { "type": "integer" } }, "required": ["action", "id"], "additionalProperties": false } }, "type": "object", "properties": { "step": { "oneOf": [ { "$ref": "#/$defs/SearchAction" }, { "$ref": "#/$defs/LookupAction" } ], "discriminator": { "propertyName": "action", "mapping": { "search": "#/$defs/SearchAction", "lookup": "#/$defs/LookupAction" } } } }, "required": ["step"], "additionalProperties": false } ``` Pydantic emits `discriminator` metadata in the generated schema, but the important part for dottxt is the `oneOf` structure and the `const` tag values on each branch. That is what makes the output unambiguous at generation time. See [AnyOf Object Variants](/json-schema/anyof-object-variants) for the schema design side of this pattern. ### Recursive models Use recursive models for trees and other nested structures where items can contain more items of the same shape. Models that reference themselves produce recursive `$defs`: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field class TreeNode(BaseModel): model_config = ConfigDict(extra="forbid") label: str children: list[TreeNode] = Field(default_factory=list, max_length=10) class TreeResponse(BaseModel): model_config = ConfigDict(extra="forbid") tree: TreeNode ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$defs": { "TreeNode": { "type": "object", "properties": { "label": { "type": "string" }, "children": { "type": "array", "items": { "$ref": "#/$defs/TreeNode" }, "maxItems": 10 } }, "required": ["label"], "additionalProperties": false } }, "type": "object", "properties": { "tree": { "$ref": "#/$defs/TreeNode" } }, "required": ["tree"], "additionalProperties": false } ``` `from __future__ import annotations` enables forward references so the model can reference itself. Set bounds on recursive lists so generation does not expand without limit. Keep the recursive type under a named object property rather than using the recursive node itself as the top-level response schema. ### Composition and inheritance Use inheritance to combine shared field groups without repeating schema definitions by hand. Use multiple inheritance to combine reusable field groups: ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict class Timestamped(BaseModel): created_at: str updated_at: str class Authored(BaseModel): author: str class Article(Timestamped, Authored): model_config = ConfigDict(extra="forbid") title: str body: str ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "created_at": { "type": "string" }, "updated_at": { "type": "string" }, "author": { "type": "string" }, "title": { "type": "string" }, "body": { "type": "string" } }, "required": ["created_at", "updated_at", "author", "title", "body"], "additionalProperties": false } ``` ### Validators do not affect schema Use validators for application-side checks, but do not rely on them to shape the generated schema. Pydantic validators run at parse time, but they do not appear in the generated JSON Schema. If you need to constrain generation, express it in the type annotation or `Field()`: ```python Won't constrain generation theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, field_validator class Invoice(BaseModel): model_config = ConfigDict(extra="forbid") amount: float currency: str @field_validator("currency") @classmethod def currency_must_be_valid(cls, v: str) -> str: if v not in ("USD", "EUR", "GBP"): raise ValueError("unsupported currency") return v ``` ```python Constrains generation theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class Invoice(BaseModel): model_config = ConfigDict(extra="forbid") amount: float currency: Literal["USD", "EUR", "GBP"] ``` ## Notes * Use Pydantic when you want Python types, runtime validation, and JSON Schema generation from one model definition. Use raw JSON Schema directly when you need full control over the output shape or keywords that do not map cleanly from Pydantic types. * Set `ConfigDict(extra="forbid")` when you want `additionalProperties: false`. * Use `Literal` for enums rather than `json_schema_extra={"enum": [...]}`. * Use validators for parse-time checks, not generation-time constraints. * See [String Bounds](/json-schema/string-bounds), [Bounded Arrays](/json-schema/bounded-arrays), [AnyOf Object Variants](/json-schema/anyof-object-variants), and [Optional vs Null](/json-schema/optional-vs-null) for schema design details that matter during generation. # TypeBox Source: https://docs.dottxt.ai/json-schema/authoring/typebox [TypeBox](https://github.com/sinclairzx81/typebox) is a TypeScript library where types **are** JSON Schema objects. No export step is needed; the schema you write is the schema you send. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} npm install @sinclair/typebox ai @ai-sdk/openai ``` ## Basic usage TypeBox types produce JSON Schema directly. Pass them to the Vercel AI SDK's `jsonSchema()` wrapper to use with dottxt: ```typescript TypeBox theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { Type, Static } from "@sinclair/typebox"; import { createOpenAI } from "@ai-sdk/openai"; import { generateObject, jsonSchema } from "ai"; const Contact = Type.Object({ name: Type.String(), email: Type.String(), role: Type.String(), }, { additionalProperties: false }); type Contact = Static; const dottxt = createOpenAI({ baseURL: "https://api.dottxt.ai/v1", apiKey: process.env.DOTTXT_API_KEY!, }); const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: jsonSchema(Contact), schemaName: "contact", prompt: "Extract: John Smith , VP Engineering", }); console.log(object.name); ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "role": { "type": "string" } }, "required": ["name", "email", "role"], "additionalProperties": false } ``` ## Adding constraints TypeBox methods map directly to JSON Schema keywords: ```typescript TypeBox theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} const UserProfile = Type.Object({ username: Type.String({ minLength: 3, maxLength: 20, pattern: "^[a-z0-9_]+$" }), bio: Type.String({ maxLength: 200 }), role: Type.Union([ Type.Literal("admin"), Type.Literal("editor"), Type.Literal("viewer"), ]), }, { additionalProperties: false }); ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "username": { "type": "string", "minLength": 3, "maxLength": 20, "pattern": "^[a-z0-9_]+$" }, "bio": { "type": "string", "maxLength": 200 }, "role": { "anyOf": [ { "const": "admin", "type": "string" }, { "const": "editor", "type": "string" }, { "const": "viewer", "type": "string" } ] } }, "required": ["username", "bio", "role"], "additionalProperties": false } ``` ## Optional and nullable fields ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} const Lead = Type.Object({ name: Type.String(), email: Type.String(), company: Type.Optional(Type.Union([Type.String(), Type.Null()])), phone: Type.Optional(Type.Union([Type.String(), Type.Null()])), }, { additionalProperties: false }); ``` `Type.Optional()` removes the field from `required`. `Type.Union([Type.String(), Type.Null()])` allows `null`. ## Arrays ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} const Survey = Type.Object({ question: Type.String(), options: Type.Array(Type.String(), { minItems: 2, maxItems: 6 }), tags: Type.Array(Type.String(), { maxItems: 5 }), }, { additionalProperties: false }); ``` ## Nested objects ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} const Address = Type.Object({ street: Type.String(), city: Type.String(), country: Type.String(), }, { additionalProperties: false }); const Customer = Type.Object({ name: Type.String(), billing_address: Address, shipping_address: Address, }, { additionalProperties: false }); ``` ## Type mapping | TypeBox | JSON Schema | TypeScript | | -------------------- | ------------------------------- | ---------------- | | `Type.String()` | `{"type": "string"}` | `string` | | `Type.Number()` | `{"type": "number"}` | `number` | | `Type.Integer()` | `{"type": "integer"}` | `number` | | `Type.Boolean()` | `{"type": "boolean"}` | `boolean` | | `Type.Null()` | `{"type": "null"}` | `null` | | `Type.Array(T)` | `{"type": "array", "items": T}` | `T[]` | | `Type.Literal("x")` | `{"const": "x"}` | `"x"` | | `Type.Union([A, B])` | `{"anyOf": [A, B]}` | `A \| B` | | `Type.Optional(T)` | removes from `required` | `T \| undefined` | | `Type.Object({...})` | `{"type": "object", ...}` | `{...}` | ## Notes * Pass `{ additionalProperties: false }` as the second argument to `Type.Object()` when you want strict object schemas. TypeBox does not set this by default, so omit it only when you intentionally want an open object. * TypeBox types are plain objects, so `JSON.stringify(Contact)` gives you the JSON Schema directly. * `Static` extracts the TypeScript type from a TypeBox schema, giving you type safety on both ends. ## Next steps * [Zod authoring](/json-schema/authoring/zod) * [Pydantic authoring](/json-schema/authoring/pydantic) * [Integrations overview](/integrations/overview) * [JSON Schema overview](/json-schema/overview) # Zod Source: https://docs.dottxt.ai/json-schema/authoring/zod [Zod](https://zod.dev/) is the most common way to define schemas in TypeScript. The Vercel AI SDK accepts Zod schemas directly; no conversion step is needed. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} npm install zod ai @ai-sdk/openai ``` ## Basic usage Pass a Zod schema to `generateObject`. The AI SDK converts it to JSON Schema and sends it to dottxt: ```typescript Zod + AI SDK theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { createOpenAI } from "@ai-sdk/openai"; import { generateObject } from "ai"; import { z } from "zod"; const dottxt = createOpenAI({ baseURL: "https://api.dottxt.ai/v1", apiKey: process.env.DOTTXT_API_KEY!, }); const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: z.object({ name: z.string(), email: z.string(), role: z.string(), }), prompt: "Extract: John Smith , VP Engineering", }); console.log(object.name); // "John Smith" ``` ```json JSON Schema (sent to API) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "role": { "type": "string" } }, "required": ["name", "email", "role"], "additionalProperties": false } ``` The AI SDK adds `additionalProperties: false` automatically when converting Zod schemas. ## Adding constraints and descriptions Zod's built-in methods translate to JSON Schema keywords. `.describe()` adds field-level descriptions that guide generation: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} const SupportTicket = z.object({ category: z.enum(["billing", "account", "bug", "feature"]) .describe("The area this ticket relates to."), priority: z.enum(["low", "medium", "high"]) .describe("How urgently this ticket needs attention."), summary: z.string().min(10).max(500) .describe("A brief description of the issue."), confidence: z.number().min(0).max(1) .describe("How confident the model is in the classification."), }).describe("A customer support ticket."); const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: SupportTicket, prompt: "Classify: I can't access my billing portal and it's blocking a renewal.", }); ``` ## Streaming Use `streamObject` to receive partial results as tokens stream in: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { streamObject } from "ai"; const { partialObjectStream } = streamObject({ model: dottxt.chat("openai/gpt-oss-20b"), schema: z.object({ title: z.string(), summary: z.string(), tags: z.array(z.string()).max(5), }), prompt: "Summarize: structured output improves LLM reliability...", }); for await (const partial of partialObjectStream) { console.log(partial); } ``` ## Notes * The AI SDK converts Zod schemas to JSON Schema before sending them to the API. To inspect the generated schema directly in Zod 4, use `z.toJSONSchema()`: ```typescript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} console.log(JSON.stringify(z.toJSONSchema(SupportTicket), null, 2)); ``` # Bounded Arrays Source: https://docs.dottxt.ai/json-schema/bounded-arrays Use array limits to control output size, cost, and downstream complexity. Without `minItems` and `maxItems`, the model decides how many array elements to produce. In practice, this means some requests return zero items (breaking downstream consumers that expect at least one) and others return dozens (blowing up storage, UI layouts, or token budgets). Array bounds make the contract explicit: you get at least N and at most M items, every time. Item-level constraints matter too. An unbounded array of unconstrained strings is effectively uncontrolled output. Adding `pattern` and `minLength`/`maxLength` to the items turns the array into a well-defined, predictable structure. ## Use case Generating article tags for search indexing. You need 3-8 tags, each short and lowercase, suitable for a tag-based search index. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9-]{2,24}$" }, "minItems": 3, "maxItems": 8 } }, "required": ["tags"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field, model_validator class TagsPayload(BaseModel): model_config = ConfigDict(extra="forbid") tags: list[str] = Field(..., min_length=3, max_length=8) @model_validator(mode="after") def validate_tags(self): if len(set(self.tags)) != len(self.tags): raise ValueError("tags must be unique") for tag in self.tags: if len(tag) < 2 or len(tag) > 24: raise ValueError("tag length must be 2-24") import re if re.match(r"^[a-z0-9-]{2,24}$", tag) is None: raise ValueError("tag must match slug pattern") return self ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const tagsPayloadSchema = z.object({ tags: z.array(z.string().regex(/^[a-z0-9-]{2,24}$/)).min(3).max(8), }).strict().superRefine((data, ctx) => { if (new Set(data.tags).size !== data.tags.length) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "tags must be unique" }); } }); ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "tags": [ "password-reset", "mobile-app", "ios", "auth-flow" ] } ``` ## Why this works `minItems: 3` guarantees you always have enough tags for meaningful search indexing. `maxItems: 8` caps generation cost and keeps tag lists manageable in UI rendering. The item-level `pattern: "^[a-z0-9-]{2,24}$"` enforces a slug format: lowercase, hyphenated, with no spaces. That means tags are already normalized for your search index without any post-processing. ## Related docs * [Object reference](/json-schema/reference/object) * [String reference](/json-schema/reference/string) # Chain of Thought Source: https://docs.dottxt.ai/json-schema/chain-of-thought Capture useful reasoning artifacts in a controlled, product-safe schema. Chain-of-thought reasoning improves model accuracy, but dumping raw reasoning text into production output creates problems: unpredictable length, no structure for reviewers to scan, and reasoning that drifts from the input text. A better approach is to capture reasoning as structured, bounded fields, such as short evidence snippets and a decision summary, rather than a freeform `"thinking"` string. This gives you the accuracy benefits of reasoning while keeping outputs auditable, compact, and machine-parseable. ## Use case You need explainable classifications for internal reviewers. Each classification should include the specific evidence from the input that drove the decision, plus a short summary a reviewer can scan in seconds. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "label": { "type": "string", "enum": ["billing", "technical", "account", "other"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 140 }, "minItems": 1, "maxItems": 3 }, "decision_summary": { "type": "string", "minLength": 20, "maxLength": 240 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["label", "evidence", "decision_summary", "confidence"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field EvidenceItem = Annotated[str, Field(min_length=10, max_length=140)] class DecisionPayload(BaseModel): model_config = ConfigDict(extra="forbid") label: Literal["billing", "technical", "account", "other"] evidence: list[EvidenceItem] = Field(..., min_length=1, max_length=3) decision_summary: str = Field(..., min_length=20, max_length=240) confidence: float = Field(..., ge=0, le=1) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const decisionSchema = z.object({ label: z.enum(["billing", "technical", "account", "other"]), evidence: z.array(z.string().min(10).max(140)).min(1).max(3), decision_summary: z.string().min(20).max(240), confidence: z.number().min(0).max(1), }).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": "Classify this support ticket. Return each evidence item as a plain English sentence quoting specific details from the ticket text. Ticket: App crashes immediately after the latest update, reproducible on Android 15 devices only." }], "response_format": { "type": "json_schema", "json_schema": { "name": "decision_payload", "schema": { "type": "object", "properties": { "label": { "type": "string", "enum": ["billing", "technical", "account", "other"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 140 }, "minItems": 1, "maxItems": 3 }, "decision_summary": { "type": "string", "minLength": 20, "maxLength": 240 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["label", "evidence", "decision_summary", "confidence"], "additionalProperties": false } } } }' ``` ## Prompt snippet ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Return concise evidence bullets and a short decision summary. Do not include hidden or verbose reasoning; keep evidence grounded in the input text. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "label": "technical", "evidence": [ "User reports crash immediately after app update.", "Issue reproduced only on Android 15 devices." ], "decision_summary": "Symptoms and reproduction details indicate an app stability bug rather than account or billing issues.", "confidence": 0.91 } ``` ## Why this works The `evidence` array (1-3 items, each 10-140 characters) forces the model to ground its reasoning in specific observations from the input, not generate vague explanations. The length bounds prevent evidence bullets from becoming mini-essays. `decision_summary` (20-240 characters) gives reviewers a one-sentence rationale without scrolling. Combined with `confidence`, it lets your application route low-confidence decisions to human review while auto-approving high-confidence ones. The fixed property order also helps. By consistently generating `label`, then `evidence`, then `decision_summary`, then `confidence`, the model follows a predictable structure instead of deciding its own output sequence on the fly. Because all reasoning fields are bounded and typed, you can store them in structured logs, aggregate them in dashboards, and search across them. None of that is practical with freeform reasoning text. ## Related docs * [String reference](/json-schema/reference/string) * [Object reference](/json-schema/reference/object) * [Classification guide](/json-schema/classification) # Classification Source: https://docs.dottxt.ai/json-schema/classification Build deterministic classifiers with enums and bounded explanation fields. Classification is one of the simplest structured output tasks, but the details matter. An unconstrained classifier might return `"Billing"`, `"billing"`, `"billing issue"`, or `"BILLING"`, all meaning the same thing but breaking your routing rules, analytics aggregations, and dashboard filters. An enum constraint eliminates this by restricting the output to exactly the values your system recognizes. Adding an `evidence` field turns a bare label into an actionable decision. Evidence gives reviewers the specific input observations that drove the classification, so they can verify quickly instead of re-reading the full ticket. ## Goal Classify support tickets into a routing queue with a label, priority, and grounded evidence. ## Schema contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "label": { "type": "string", "enum": ["billing", "technical", "account", "shipping"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 160 }, "minItems": 1, "maxItems": 3 } }, "required": ["label", "priority", "evidence"], "additionalProperties": false }, { "type": "object", "properties": { "label": { "type": "string", "const": "other" }, "other_label": { "type": "string", "minLength": 3, "maxLength": 60 }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 160 }, "minItems": 1, "maxItems": 3 } }, "required": ["label", "other_label", "priority", "evidence"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field EvidenceItem = Annotated[str, Field(min_length=10, max_length=160)] class BaseClassification(BaseModel): model_config = ConfigDict(extra="forbid") priority: Literal["low", "medium", "high", "urgent"] evidence: list[EvidenceItem] = Field(..., min_length=1, max_length=3) class KnownClassification(BaseClassification): label: Literal["billing", "technical", "account", "shipping"] class OtherClassification(BaseClassification): label: Literal["other"] other_label: str = Field(..., min_length=3, max_length=60) Classification = Annotated[KnownClassification | OtherClassification, Field(discriminator="label")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const common = { priority: z.enum(["low", "medium", "high", "urgent"]), evidence: z.array(z.string().min(10).max(160)).min(1).max(3), }; const classificationSchema = z.discriminatedUnion("label", [ z.object({ label: z.enum(["billing", "technical", "account", "shipping"]), ...common }).strict(), z.object({ label: z.literal("other"), other_label: z.string().min(3).max(60), ...common }).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": "My card was charged twice for order ORD-9842. Need refund today." }], "response_format": { "type": "json_schema", "json_schema": { "name": "ticket_classification", "schema": { "oneOf": [ { "type": "object", "properties": { "label": { "type": "string", "enum": ["billing", "technical", "account", "shipping"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 160 }, "minItems": 1, "maxItems": 3 } }, "required": ["label", "priority", "evidence"], "additionalProperties": false }, { "type": "object", "properties": { "label": { "type": "string", "const": "other" }, "other_label": { "type": "string", "minLength": 3, "maxLength": 60 }, "priority": { "type": "string", "enum": ["low", "medium", "high", "urgent"] }, "evidence": { "type": "array", "items": { "type": "string", "minLength": 10, "maxLength": 160 }, "minItems": 1, "maxItems": 3 } }, "required": ["label", "other_label", "priority", "evidence"], "additionalProperties": false } ] } } } }' ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} My card was charged twice for order ORD-9842. Need refund today. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "label": "billing", "priority": "high", "evidence": [ "User reports duplicate card charge.", "Explicit refund request indicates financial impact." ] } ``` ## Implementation tips * **Enum labels are non-negotiable.** Every label value should map directly to a routing queue, a dashboard filter, or a database category. If the label isn't in the enum, it can't reach your system. * **Evidence grounds the decision.** The bounded evidence array (1-3 items, 10-160 characters each) forces the model to cite specific observations from the input. This makes quality audits fast: reviewers check evidence against the ticket text instead of guessing why the model chose a label. * **Fallback for taxonomy drift.** The second `oneOf` branch requires `other_label` when `label` is `"other"`. This captures novel categories without polluting your main enum. Review `other_label` values periodically and promote frequent ones into the enum. See [Enum with Fallback](/json-schema/enum-with-fallback). ## Related docs * [Enum with fallback](/json-schema/enum-with-fallback): handle novel categories without polluting your main enum * [Unions of objects](/json-schema/union-of-objects): discriminated unions for routing to different output shapes * [Bounded arrays](/json-schema/bounded-arrays): control the size of evidence and tag arrays * [String reference](/json-schema/reference/string) | [Conditionals reference](/json-schema/reference/conditionals) # Conditional Requirements Source: https://docs.dottxt.ai/json-schema/conditional-requirements Require different fields based on the value of another field. Some requirements depend on a field's value, not just its presence. If `delivery_method` is `"shipping"`, you need a full address. If it is `"pickup"`, you need a pickup location instead. JSON Schema's `if` / `then` / `else` keywords let you express that branching logic directly in the schema. This is different from `dependentRequired`. Use `dependentRequired` when fields travel together based only on presence. Use `if` / `then` / `else` when the required fields depend on a specific value. Pydantic and Zod do not generate JSON Schema with `if` / `then` / `else`. If you need conditional constraints, write the JSON Schema directly. ## Use case Checkout fulfillment where `delivery_method="shipping"` requires `address`, while `delivery_method="pickup"` requires `pickup_location`. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "delivery_method": { "type": "string", "enum": ["shipping", "pickup"] }, "address": { "type": "string", "minLength": 10, "maxLength": 200 }, "pickup_location": { "type": "string", "enum": ["warehouse-a", "warehouse-b", "storefront"] } }, "required": ["delivery_method"], "if": { "properties": { "delivery_method": { "const": "shipping" } }, "required": ["delivery_method"] }, "then": { "required": ["address"] }, "else": { "required": ["pickup_location"] }, "additionalProperties": false } ``` ```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": "Customer wants to pick up the order at warehouse-a." }], "response_format": { "type": "json_schema", "json_schema": { "name": "fulfillment_details", "schema": { "type": "object", "properties": { "delivery_method": { "type": "string", "enum": ["shipping", "pickup"] }, "address": { "type": "string", "minLength": 10, "maxLength": 200 }, "pickup_location": { "type": "string", "enum": ["warehouse-a", "warehouse-b", "storefront"] } }, "required": ["delivery_method"], "if": { "properties": { "delivery_method": { "const": "shipping" } }, "required": ["delivery_method"] }, "then": { "required": ["address"] }, "else": { "required": ["pickup_location"] }, "additionalProperties": false } } } }' ``` ## Example outputs Shipping: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "delivery_method": "shipping", "address": "10 Main Street, Austin, TX 78701" } ``` Pickup: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "delivery_method": "pickup", "pickup_location": "warehouse-a" } ``` ## Why this works Without conditional requirements, the model might produce a shipping request with no address, or a pickup request with no pickup location. Your application would then need to infer which fields are missing and retry or repair the payload. `if` / `then` / `else` prevents that at generation time. The model sees that the required field set changes with `delivery_method`, and it produces the matching branch. Use this pattern when the decision depends on a value. For presence-only dependencies like "if `vat_id` exists, require `billing_country`", use [Field Dependencies](/json-schema/field-dependencies). ## Related docs * [Conditionals reference](/json-schema/reference/conditionals) * [Field Dependencies](/json-schema/field-dependencies) * [Union of Objects](/json-schema/union-of-objects) # Content Generation Source: https://docs.dottxt.ai/json-schema/content-generation Generate marketing or product copy in a strict, publishable structure. Content generation without structure produces a blob of text that someone has to manually split into headline, body, and a call to action (CTA). If the model writes a 200-character headline, your page layout breaks. If it forgets the call to action, your marketing page has no conversion path. If the tone drifts from professional to casual mid-paragraph, the brand voice is inconsistent. Wrapping content generation in a schema solves all of this. Each section becomes a separate field with its own length bounds. Tone becomes an enum, not a suggestion. The CTA is a required object with a label and URL. The model fills in the creative content while the schema enforces the structural and editorial constraints. ## Goal Generate a product launch blurb with a headline, subheadline, key points, call to action (CTA), and tone, all bounded and validated for direct web publishing. ## Schema contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "headline": { "type": "string", "minLength": 15, "maxLength": 80 }, "subheadline": { "type": "string", "minLength": 25, "maxLength": 140 }, "key_points": { "type": "array", "minItems": 3, "maxItems": 5, "items": { "type": "string", "minLength": 20, "maxLength": 140 } }, "cta": { "type": "object", "properties": { "label": { "type": "string", "minLength": 2, "maxLength": 24 }, "url": { "type": "string", "pattern": "^https://" } }, "required": ["label", "url"], "additionalProperties": false }, "tone": { "type": "string", "enum": ["professional", "friendly", "technical"] } }, "required": ["headline", "subheadline", "key_points", "cta", "tone"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field KeyPoint = Annotated[str, Field(min_length=20, max_length=140)] class Cta(BaseModel): model_config = ConfigDict(extra="forbid") label: str = Field(..., min_length=2, max_length=24) url: str = Field(..., pattern=r"^https://") class ContentPayload(BaseModel): model_config = ConfigDict(extra="forbid") headline: str = Field(..., min_length=15, max_length=80) subheadline: str = Field(..., min_length=25, max_length=140) key_points: list[KeyPoint] = Field(..., min_length=3, max_length=5) cta: Cta tone: Literal["professional", "friendly", "technical"] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const contentPayloadSchema = z.object({ headline: z.string().min(15).max(80), subheadline: z.string().min(25).max(140), key_points: z.array(z.string().min(20).max(140)).min(3).max(5), cta: z.object({ label: z.string().min(2).max(24), url: z.string().regex(/^https:\/\//), }).strict(), tone: z.enum(["professional", "friendly", "technical"]), }).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": "Announce a new schema audit service for engineering teams.\nTone should be professional and concrete." }], "response_format": { "type": "json_schema", "json_schema": { "name": "content_payload", "schema": { "type": "object", "properties": { "headline": { "type": "string", "minLength": 15, "maxLength": 80 }, "subheadline": { "type": "string", "minLength": 25, "maxLength": 140 }, "key_points": { "type": "array", "minItems": 3, "maxItems": 5, "items": { "type": "string", "minLength": 20, "maxLength": 140 } }, "cta": { "type": "object", "properties": { "label": { "type": "string", "minLength": 2, "maxLength": 24 }, "url": { "type": "string", "pattern": "^https://" } }, "required": ["label", "url"], "additionalProperties": false }, "tone": { "type": "string", "enum": ["professional", "friendly", "technical"] } }, "required": ["headline", "subheadline", "key_points", "cta", "tone"], "additionalProperties": false } } } }' ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Announce a new schema audit service for engineering teams. Tone should be professional and concrete. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "headline": "Ship More Reliable Structured Outputs With Schema Audit", "subheadline": "Get a concrete review of your JSON Schema constraints, failure modes, and production readiness before rollout.", "key_points": [ "Identify underconstrained fields that lead to invalid or noisy model output.", "Receive practical schema rewrites tied to your real production use cases.", "Validate compatibility and behavior before launching high-volume workflows." ], "cta": { "label": "Start Schema Audit", "url": "https://example.com/audit" }, "tone": "professional" } ``` ## Implementation tips * **Tone enum controls voice.** `"professional"`, `"friendly"`, `"technical"`: the model adopts the specified style without you embedding tone instructions in the prompt. Add or remove values as your brand guidelines evolve. * **Length bounds match your layout.** The headline is capped at 80 characters (fits a single line on desktop), the subheadline at 140 (fits two lines), and key points at 140 each. These bounds should come from your actual CSS/layout constraints. * **Key points array is bounded.** `minItems: 3` ensures enough substance for a marketing page. `maxItems: 5` prevents the model from listing fifteen points that nobody reads. * **CTA is required and structured.** The `cta` object guarantees every generated blurb has a clear action. The `pattern: "^https://"` on `url` prevents relative paths, `mailto:` links, or other formats your publishing system doesn't support. * **Human review is still needed.** The schema handles structure and bounds, but brand voice, factual accuracy, and legal compliance still need human eyes. Schema-constrained output makes this review faster because the structure is predictable. ## Related docs * [String bounds](/json-schema/string-bounds): control minLength, maxLength, pattern, and format on text fields * [Bounded arrays](/json-schema/bounded-arrays): set min/max item counts on key points, bullet lists, etc. * [Enum with fallback](/json-schema/enum-with-fallback): extend tone or category enums with an escape hatch * [String reference](/json-schema/reference/string) | [Object reference](/json-schema/reference/object) # Data Extraction Source: https://docs.dottxt.ai/json-schema/data-extraction Extract high-value fields from unstructured text into strict JSON ready for storage. Data extraction from unstructured text, including invoices, receipts, medical records, and contracts, is the bread and butter of structured output. The schema defines what “correctly extracted” looks like: which fields must be present, what types they have, and what ranges are valid. This turns extraction from a fuzzy NLP task into a well-defined contract: the output either conforms to the schema or it doesn't. The tighter your schema constraints, the less post-processing you need. A `format: “date”` constraint on the invoice date means you get `”2026-02-12”` instead of `”Feb 12, 2026”` or `”12/02/2026”`. A `pattern` constraint on currency codes means you get `”USD”` instead of `”US Dollars”`. ## Goal Extract invoice data from OCR text into a normalized, storage-ready record with validated types and bounded arrays. ## Schema contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "invoice_id": { "type": "string", "minLength": 1, "maxLength": 40 }, "vendor": { "type": "string", "minLength": 1, "maxLength": 120 }, "invoice_date": { "type": "string", "format": "date" }, "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, "total": { "type": "number", "minimum": 0 }, "line_items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1, "maxLength": 200 }, "quantity": { "type": "number", "minimum": 0 }, "unit_price": { "type": "number", "minimum": 0 }, "line_total": { "type": "number", "minimum": 0 } }, "required": ["description", "quantity", "unit_price", "line_total"], "additionalProperties": false } } }, "required": ["invoice_id", "vendor", "invoice_date", "currency", "total", "line_items"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field from datetime import date class LineItem(BaseModel): model_config = ConfigDict(extra="forbid") description: str = Field(..., min_length=1, max_length=200) quantity: float = Field(..., ge=0) unit_price: float = Field(..., ge=0) line_total: float = Field(..., ge=0) class InvoiceRecord(BaseModel): model_config = ConfigDict(extra="forbid") invoice_id: str = Field(..., min_length=1, max_length=40) vendor: str = Field(..., min_length=1, max_length=120) invoice_date: date currency: str = Field(..., pattern=r"^[A-Z]{3}$") total: float = Field(..., ge=0) line_items: list[LineItem] = Field(..., min_length=1, max_length=100) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const invoiceRecordSchema = z.object({ invoice_id: z.string().min(1).max(40), vendor: z.string().min(1).max(120), invoice_date: z.iso.date(), currency: z.string().regex(/^[A-Z]{3}$/), total: z.number().min(0), line_items: z.array(z.object({ description: z.string().min(1).max(200), quantity: z.number().min(0), unit_price: z.number().min(0), line_total: z.number().min(0), }).strict()).min(1).max(100), }).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": "Invoice INV-22019\nVendor: Northwind Supplies\nDate: 2026-02-12\nCurrency: USD\nItems:\n- Battery Pack x2 @ 39.50 = 79.00\n- Cable Kit x1 @ 15.00 = 15.00\nTotal: 94.00" }], "response_format": { "type": "json_schema", "json_schema": { "name": "invoice_record", "schema": { "type": "object", "properties": { "invoice_id": { "type": "string", "minLength": 1, "maxLength": 40 }, "vendor": { "type": "string", "minLength": 1, "maxLength": 120 }, "invoice_date": { "type": "string", "format": "date" }, "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, "total": { "type": "number", "minimum": 0 }, "line_items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "properties": { "description": { "type": "string", "minLength": 1, "maxLength": 200 }, "quantity": { "type": "number", "minimum": 0 }, "unit_price": { "type": "number", "minimum": 0 }, "line_total": { "type": "number", "minimum": 0 } }, "required": ["description", "quantity", "unit_price", "line_total"], "additionalProperties": false } } }, "required": ["invoice_id", "vendor", "invoice_date", "currency", "total", "line_items"], "additionalProperties": false } } } }' ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Invoice INV-22019 Vendor: Northwind Supplies Date: 2026-02-12 Currency: USD Items: - Battery Pack x2 @ 39.50 = 79.00 - Cable Kit x1 @ 15.00 = 15.00 Total: 94.00 ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "invoice_id": "INV-22019", "vendor": "Northwind Supplies", "invoice_date": "2026-02-12", "currency": "USD", "total": 94, "line_items": [ { "description": "Battery Pack", "quantity": 2, "unit_price": 39.5, "line_total": 79 }, { "description": "Cable Kit", "quantity": 1, "unit_price": 15, "line_total": 15 } ] } ``` ## Implementation tips * **Narrow fields to business needs.** Don't add a catch-all `"raw_text"` field. Each field should map to a column in your database or a field in your downstream API. If you don't need it, don't extract it. * **Bound arrays to realistic limits.** `maxItems: 100` on line items is generous but prevents runaway generation on malformed OCR input. Without it, a noisy scan could produce thousands of phantom line items. * **Use `format` and `pattern` for normalization.** `format: "date"` on `invoice_date` gives you ISO 8601 dates regardless of how the source text formats them. `pattern: "^[A-Z]{3}$"` on `currency` gives you three-letter codes, not spelled-out currency names. * **Consider per-field confidence.** For high-stakes extraction (financial documents, medical records), add a `confidence` number field next to each extracted value. This lets your application flag low-confidence extractions for human review rather than trusting everything equally. ## Related docs * [Optional fields](/json-schema/optional-fields): make fields the model can omit when the source text doesn't contain them * [Optional vs Null](/json-schema/optional-vs-null): choose between "field absent" and "field present but null" * [String bounds](/json-schema/string-bounds): control length, format, and regex on extracted strings * [Bounded arrays](/json-schema/bounded-arrays): set min/max item counts on repeated structures * [Object reference](/json-schema/reference/object) | [String reference](/json-schema/reference/string) # dottxt SDK Source: https://docs.dottxt.ai/json-schema/dottxt-sdk Pass Python types directly to the dottxt SDK without authoring JSON Schema by hand. The [dottxt Python SDK](https://github.com/dottxt-ai/dottxt-python) accepts a wide range of Python types as `response_format` on `generate(...)`, for many shapes you don't need to write or generate JSON Schema at all. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install dottxt ``` ## Supported `response_format` types `DotTxt.generate(...)` and `AsyncDotTxt.generate(...)` accept `response_format` as any of: * a Pydantic model class * a TypedDict type * a dataclass type * an Enum class * a `typing.Literal[...]` type * a `typing.Union[...]` type * a `typing.Optional[...]` type * typed containers such as `list[...]`, `dict[...]`, `tuple[...]` * a JSON string containing JSON Schema * a JSON object (`dict`) The return type follows the input: a Pydantic model class returns a validated model instance, and other supported types return parsed JSON. ## Pydantic model Use a Pydantic model when you want runtime validation alongside generation. The result is a validated model instance. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, Field from dottxt import DotTxt class IncidentSummary(BaseModel): severity: Literal["low", "medium", "high"] team: str = Field(max_length=32) client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input=( "Summarize this incident: checkout errors are blocking purchases. " "Return a JSON object with keys severity and team." ), response_format=IncidentSummary, ) print(result) # severity='high' team='checkout' print(result.model_dump()) # {'severity': 'high', 'team': 'checkout'} ``` See [Pydantic](/json-schema/authoring/pydantic) for the full schema mapping. ## TypedDict Use a `TypedDict` for a lightweight class declaration without Pydantic as a dependency. The result is a plain `dict`. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from __future__ import annotations from typing import Literal, TypedDict from dottxt import DotTxt class IncidentPayload(TypedDict): severity: Literal["low", "medium", "high"] team: str client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input=( "Summarize this incident: checkout errors are blocking purchases. " "Return a JSON object with keys severity and team." ), response_format=IncidentPayload, ) print(result) # {'severity': 'high', 'team': 'checkout'} ``` ## Dataclass Standard library `@dataclass` types are supported as well. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from __future__ import annotations from dataclasses import dataclass from typing import Literal from dottxt import DotTxt @dataclass class IncidentPayload: severity: Literal["low", "medium", "high"] team: str client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input=( "Summarize this incident: checkout errors are blocking purchases. " "Return a JSON object with keys severity and team." ), response_format=IncidentPayload, ) print(result) # {'severity': 'high', 'team': 'checkout'} ``` ## Enum and Literal Pass an `Enum` class or a `typing.Literal[...]` when the entire output is a single value drawn from a fixed set. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from enum import Enum from typing import Literal from dottxt import DotTxt class Severity(str, Enum): low = "low" medium = "medium" high = "high" client = DotTxt() severity = client.generate( model="openai/gpt-oss-20b", input="Classify the severity of: checkout errors are blocking purchases.", response_format=Severity, ) print(severity) # 'high' label = client.generate( model="openai/gpt-oss-20b", input="Classify the sentiment of: 'I love this product!'", response_format=Literal["positive", "negative", "neutral"], ) print(label) # 'positive' ``` ## Union and Optional `typing.Union[...]` and `typing.Optional[...]` constrain the output to one of several types. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Optional, Union from dottxt import DotTxt client = DotTxt() value = client.generate( model="openai/gpt-oss-20b", input="How many incidents this week? Reply with a number, or null if unknown.", response_format=Optional[int], ) print(value) mixed = client.generate( model="openai/gpt-oss-20b", input="Reply with the user's age as an integer, or their name as a string.", response_format=Union[int, str], ) print(mixed) ``` ## Typed containers Typed containers like `list[...]`, `dict[...]`, and `tuple[...]` work directly. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from dottxt import DotTxt client = DotTxt() teams = client.generate( model="openai/gpt-oss-20b", input="List three engineering teams that own checkout systems.", response_format=list[str], ) print(teams) # ['checkout', 'payments', 'orders'] scores = client.generate( model="openai/gpt-oss-20b", input="Score each team's incident impact on a scale of 0-10.", response_format=dict[str, int], ) print(scores) # {'checkout': 9, 'payments': 6, 'orders': 4} ``` ## JSON Schema (string or dict) You can also pass JSON Schema directly as a Python `dict` or as a JSON string. This is useful when you have a schema authored elsewhere — by hand, by [Quicktype](/json-schema/quicktype), by [Genson](/json-schema/genson), or shared from another service. ```python Python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from dottxt import DotTxt schema = { "type": "object", "properties": { "severity": {"type": "string", "enum": ["low", "medium", "high"]}, "team": {"type": "string", "maxLength": 32}, }, "required": ["severity", "team"], "additionalProperties": False, } client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input="Summarize this incident: checkout errors are blocking purchases.", response_format=schema, ) print(result) # {'severity': 'high', 'team': 'checkout'} ``` # Enum With Fallback Source: https://docs.dottxt.ai/json-schema/enum-with-fallback Use controlled categories with an explicit `other` escape hatch. Enums are the strongest constraint for categorical fields: the output must match one of the listed values exactly. But real-world taxonomies drift. A new product launches, a new issue type appears, and suddenly a significant fraction of inputs don't fit any existing category. If your enum has no escape hatch, the model is forced to pick the closest match, which introduces silent misclassification. The fallback pattern adds an `"other"` value and models it as a separate union branch that requires a freeform description. This preserves the analytical benefits of a closed enum for known categories while capturing novel ones accurately. ## Use case Issue triage where most tickets fit known categories (`billing`, `technical`, `account`, `shipping`), but some do not, and you need to capture what they actually are instead of forcing a bad fit. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "issue_type": { "type": "string", "enum": ["billing", "technical", "account", "shipping"] } }, "required": ["issue_type"], "additionalProperties": false }, { "type": "object", "properties": { "issue_type": { "type": "string", "const": "other" }, "other_issue_type": { "type": "string", "minLength": 3, "maxLength": 60 } }, "required": ["issue_type", "other_issue_type"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field class KnownIssue(BaseModel): model_config = ConfigDict(extra="forbid") issue_type: Literal["billing", "technical", "account", "shipping"] class OtherIssue(BaseModel): model_config = ConfigDict(extra="forbid") issue_type: Literal["other"] other_issue_type: str = Field(..., min_length=3, max_length=60) IssuePayload = Annotated[KnownIssue | OtherIssue, Field(discriminator="issue_type")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const issuePayloadSchema = z.discriminatedUnion("issue_type", [ z.object({ issue_type: z.enum(["billing", "technical", "account", "shipping"]), }).strict(), z.object({ issue_type: z.literal("other"), other_issue_type: z.string().min(3).max(60), }).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": "My login stopped working after the latest app update." }], "response_format": { "type": "json_schema", "json_schema": { "name": "issue_triage", "schema": { "oneOf": [ { "type": "object", "properties": { "issue_type": { "type": "string", "enum": ["billing", "technical", "account", "shipping"] } }, "required": ["issue_type"], "additionalProperties": false }, { "type": "object", "properties": { "issue_type": { "type": "string", "const": "other" }, "other_issue_type": { "type": "string", "minLength": 3, "maxLength": 60 } }, "required": ["issue_type", "other_issue_type"], "additionalProperties": false } ] } } } }' ``` ## Example outputs Known category: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "issue_type": "technical" } ``` Fallback category: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "issue_type": "other", "other_issue_type": "partner-api-timeout" } ``` ## Why this works For the vast majority of inputs, `issue_type` is one of the four known values. Your analytics dashboards, routing rules, and reports all work unchanged. When a genuinely novel category appears, the model selects `"other"` and the second `oneOf` branch requires `other_issue_type`, a bounded freeform string that captures what the issue actually is. This gives you the best of both worlds: a closed enum for known categories (fast aggregation, reliable routing) and a structured escape hatch for new ones (no data loss, easy to review). Over time, you can promote frequently-seen `other_issue_type` values into the main enum. ## Related docs * [Unions of objects](/json-schema/union-of-objects) * [Object reference](/json-schema/reference/object) * [Classification guide](/json-schema/classification) # Field Dependencies Source: https://docs.dottxt.ai/json-schema/field-dependencies Require fields only when logically related fields are present. Some fields only make sense together. A VAT ID without a billing country is useless because you can't validate it, route it, or report on it. A shipping tracking number without a carrier name is just a random string. `dependentRequired` encodes these relationships directly in the schema: if field A appears, fields B and C must also appear. This is simpler than `if`/`then` because it doesn't depend on a field's value, only its presence. It's the right tool when fields travel in groups. ## Use case Customer billing details where `vat_id`, `company_name`, and `billing_country` must all appear together. If the customer is a business with a VAT ID, you need the other two fields to validate and process it. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "customer_type": { "type": "string", "enum": ["individual", "business"] }, "company_name": { "type": "string", "minLength": 1, "maxLength": 120 }, "vat_id": { "type": "string", "pattern": "^[A-Z0-9-]{6,20}$" }, "billing_country": { "type": "string", "pattern": "^[A-Z]{2}$" } }, "required": ["customer_type"], "dependentRequired": { "vat_id": ["company_name", "billing_country"] }, "additionalProperties": false } ``` Pydantic and Zod can validate the same rule in application code, but they do not emit `dependentRequired` in the generated JSON Schema. For structured generation, prefer raw JSON Schema whenever this dependency needs to be part of the contract you send to the model. ```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": "Business customer: Acme Logistics, VAT ID FR-12345678, billing country France (FR)." }], "response_format": { "type": "json_schema", "json_schema": { "name": "billing_details", "schema": { "type": "object", "properties": { "customer_type": { "type": "string", "enum": ["individual", "business"] }, "company_name": { "type": "string", "minLength": 1, "maxLength": 120 }, "vat_id": { "type": "string", "pattern": "^[A-Z0-9-]{6,20}$" }, "billing_country": { "type": "string", "pattern": "^[A-Z]{2}$" } }, "required": ["customer_type"], "dependentRequired": { "vat_id": ["company_name", "billing_country"] }, "additionalProperties": false } } } }' ``` ## Example outputs Valid business record: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "customer_type": "business", "company_name": "Acme Logistics", "vat_id": "FR-12345678", "billing_country": "FR" } ``` Invalid pattern to avoid (missing dependency): ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "customer_type": "business", "vat_id": "FR-12345678" } ``` ## Why this works Without `dependentRequired`, the model might produce `{"customer_type": "business", "vat_id": "FR-12345678"}`: a business record with a VAT ID but no company name or billing country. Your application would then need to either reject the record and retry, or patch the missing fields from another source. Both options are expensive. `dependentRequired` prevents this at generation time. The model sees the constraint and produces all related fields together, or none of them. This keeps your application code simple: you validate once and process, rather than validate-then-repair. If this dependency needs to be part of the generated schema contract, use raw JSON Schema in `response_format`. Use the Pydantic and Zod patterns only when you are validating after generation inside your own application. ## Related docs * [Conditionals reference](/json-schema/reference/conditionals) * [Object reference](/json-schema/reference/object) # Form Processing Source: https://docs.dottxt.ai/json-schema/form-processing Normalize unstructured form text into a strict backend payload with validation-friendly fields. Users type addresses with inconsistent formatting, phone numbers with or without country codes, and shipping preferences in plain English. If you pass this text to your backend as-is, you need normalization logic, validation logic, and error handling for every possible format variation. Schema-constrained generation does this normalization at extraction time: the model reads the messy input and produces a clean, typed object that conforms to your backend's expectations. ## Goal Convert mixed-quality user submissions into a normalized order payload with correct types, validated formats, and consistent structure, ready for your order creation API. ## Schema contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "customer": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "email": { "type": "string", "format": "email" }, "phone": { "type": ["string", "null"], "pattern": "^\\+?[1-9][0-9]{7,14}$" } }, "required": ["name", "email", "phone"], "additionalProperties": false }, "shipping": { "type": "object", "properties": { "method": { "type": "string", "enum": ["standard", "express"] }, "address_line1": { "type": "string", "minLength": 1, "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": ["method", "address_line1", "city", "postal_code", "country"], "additionalProperties": false }, "notes": { "type": ["string", "null"], "maxLength": 300 } }, "required": ["customer", "shipping"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict, EmailStr, Field class Customer(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(..., min_length=1, max_length=100) email: EmailStr phone: str | None = Field(..., pattern=r"^\+?[1-9][0-9]{7,14}$") class Shipping(BaseModel): model_config = ConfigDict(extra="forbid") method: Literal["standard", "express"] address_line1: str = Field(..., min_length=1, max_length=120) city: str = Field(..., min_length=1, max_length=80) postal_code: str = Field(..., min_length=3, max_length=20) country: str = Field(..., pattern=r"^[A-Z]{2}$") class OrderPayload(BaseModel): model_config = ConfigDict(extra="forbid") customer: Customer shipping: Shipping notes: str | None = Field(None, max_length=300) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const orderPayloadSchema = z.object({ customer: z.object({ name: z.string().min(1).max(100), email: z.string().email(), phone: z.string().regex(/^\+?[1-9][0-9]{7,14}$/).nullable(), }).strict(), shipping: z.object({ method: z.enum(["standard", "express"]), address_line1: z.string().min(1).max(120), city: z.string().min(1).max(80), postal_code: z.string().min(3).max(20), country: z.string().regex(/^[A-Z]{2}$/), }).strict(), notes: z.string().max(300).nullable().optional(), }).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": "Ship this to 10 Main Street, Austin 78701 US. Use express please. Name: Alice Johnson, email alice@acme.com. No phone." }], "response_format": { "type": "json_schema", "json_schema": { "name": "order_payload", "schema": { "type": "object", "properties": { "customer": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "email": { "type": "string", "format": "email" }, "phone": { "type": ["string", "null"], "pattern": "^\\+?[1-9][0-9]{7,14}$" } }, "required": ["name", "email", "phone"], "additionalProperties": false }, "shipping": { "type": "object", "properties": { "method": { "type": "string", "enum": ["standard", "express"] }, "address_line1": { "type": "string", "minLength": 1, "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": ["method", "address_line1", "city", "postal_code", "country"], "additionalProperties": false }, "notes": { "type": ["string", "null"], "maxLength": 300 } }, "required": ["customer", "shipping"], "additionalProperties": false } } } }' ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Ship this to 10 Main Street, Austin 78701 US. Use express please. I'm Alice Johnson, email alice@acme.com. No phone. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "customer": { "name": "Alice Johnson", "email": "alice@acme.com", "phone": null }, "shipping": { "method": "express", "address_line1": "10 Main Street", "city": "Austin", "postal_code": "78701", "country": "US" } } ``` ## Implementation tips * **Nullable for "asked but absent."** The user explicitly said "no phone," so `phone` is `null` rather than omitted. Your backend can distinguish "no phone provided" from "phone not asked," which is useful for follow-up workflows. * **Enums for controlled vocabulary.** `method` is `"standard"` or `"express"`, not `"fast"`, `"next day"`, or `"ASAP"`. The enum forces normalization so your shipping logic doesn't need string matching. * **Pattern for format enforcement.** The `country` field uses `"^[A-Z]{2}$"` so the model produces `"US"` instead of `"United States"`, `"usa"`, or `"U.S.A."`. The same idea applies to `phone`: the E.164-like pattern ensures a format your telephony API accepts. * **`additionalProperties: false`** prevents the model from forwarding raw user text as extra fields, which could leak PII into systems that don't expect it. ## Related docs * [Optional vs Null](/json-schema/optional-vs-null): distinguish "not provided" from "explicitly absent" * [Optional fields](/json-schema/optional-fields): fields the model can omit entirely * [String bounds](/json-schema/string-bounds): control length, format, and regex on form fields * [Object reference](/json-schema/reference/object) | [String reference](/json-schema/reference/string) # Genson Source: https://docs.dottxt.ai/json-schema/genson Infer JSON Schema from JSON instances using Genson in Python. [Genson](https://github.com/wolverdude/GenSON) is a Python library that builds a JSON Schema by observing JSON instances. Feed it one or more examples and it produces a schema that accepts all of them. Genson includes a top-level `"$schema"` field in its default output. It is omitted from the examples below for readability. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install genson ``` ## Basic usage ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from genson import SchemaBuilder builder = SchemaBuilder() builder.add_object({ "name": "Alice Johnson", "email": "alice@acme.com", "role": "Product Manager" }) print(builder.to_json(indent=2)) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "role": { "type": "string" } }, "required": ["email", "name", "role"] } ``` Genson infers types from values and marks all observed fields as required. Note that Genson sorts `required` alphabetically and does not add `additionalProperties: false`; you should add that manually. ## Multiple instances Genson's strength is incremental learning. Feed it several examples and it merges them, detecting which fields are always present (required) and which appear only sometimes (optional): ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_object({"name": "Alice", "email": "alice@acme.com", "phone": "+1-555-0100"}) builder.add_object({"name": "Bob", "email": "bob@acme.com"}) builder.add_object({"name": "Carol", "email": "carol@acme.com", "phone": "+1-555-0102"}) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "phone": { "type": "string" } }, "required": ["email", "name"] } ``` Since `phone` is missing from the second example, it becomes optional while `name` and `email` stay required. ## Nested objects Genson handles nested structures, inferring a full sub-schema for each nested object: ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_object({ "name": "Alice", "address": { "street": "123 Main St", "city": "Springfield", "country": "US" } }) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "country": { "type": "string" } }, "required": ["city", "country", "street"] } }, "required": ["address", "name"] } ``` Unlike Pydantic or Quicktype, Genson inlines nested objects rather than extracting them into `$defs`. ## Arrays Genson infers array item types from the elements it sees: ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_object({ "question": "What is your favorite color?", "options": ["red", "green", "blue"], "tags": ["survey", "color"] }) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "question": { "type": "string" }, "options": { "type": "array", "items": { "type": "string" } }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["options", "question", "tags"] } ``` Genson does not add `minItems` or `maxItems`; add those manually. See [Bounded Arrays](/json-schema/bounded-arrays). ## Arrays of objects When arrays contain objects, Genson infers the item schema by merging all observed elements: ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_object({ "contacts": [ {"name": "Alice", "email": "alice@acme.com"}, {"name": "Bob", "email": "bob@acme.com"} ] }) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "contacts": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } }, "required": ["email", "name"] } } }, "required": ["contacts"] } ``` ## Mixed types When the same field has different types across samples, Genson produces a type union: ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_object({"value": 42}) builder.add_object({"value": "hello"}) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "value": { "type": ["integer", "string"] } }, "required": ["value"] } ``` ## Seeding with an existing schema You can start from a hand-written schema and let Genson extend it with fields observed in data. Constraints from the seed schema (like `enum`) are preserved: ```python Genson theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} builder = SchemaBuilder() builder.add_schema({ "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "account", "bug"] } }, "required": ["category"] }) builder.add_object({"category": "billing", "summary": "Can't login"}) ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "account", "bug"] }, "summary": { "type": "string" } }, "required": ["category"] } ``` The `enum` constraint on `category` is preserved from the seed. `summary` is added to `properties` but not to `required` since it was not in the seed's `required` list. ## Limitations Genson infers structure and types but does not add semantic constraints. The generated schema will not include: * `enum` values (unless seeded) * `minLength`, `maxLength`, or `pattern` for strings * `minItems`, `maxItems` for arrays * `additionalProperties: false` on objects * `description` on fields or the schema The output is a starting point. Tighten it by adding constraints manually or follow [Improve your schema](/json-schema/improve-your-schema). # Improve Your Schema Source: https://docs.dottxt.ai/json-schema/improve-your-schema Turn prior knowledge into explicit constraints for more reliable structured outputs. Every schema we review has the same problem: it describes the shape of the data but not its boundaries. Fields that should be enums are bare strings. Arrays have no size limits. Formats are described in `description` instead of enforced with `pattern`. The schema looks correct, the output is structurally valid, and then something downstream breaks because a "summary" came back as 3,000 characters or a "status" came back as `"In Progress (Pending Review)"`. The fix is always the same: encode what you already know. If status has four valid values, use an enum. If a summary feeds into a 240-character column, set `maxLength: 240`. If a date must be ISO 8601, use `format: "date"`. The model works within whatever constraints you give it; the question is whether you give it enough. ## Use enums for known values If `status` is always one of four workflow states, say so. A bare `"type": "string"` lets the model return `"open"`, `"Open"`, `"OPEN"`, `"currently open"`, or anything else. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "status": { "type": "string" } }, "required": ["status"] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "status": { "type": "string", "enum": ["open", "in_progress", "resolved", "closed"] } }, "required": ["status"], "additionalProperties": false } ``` ## Distinguish optional from nullable "Not provided" and "explicitly empty" are different states. If your code treats them the same, you can't tell whether a field was never captured or was captured and found to be missing, and that ambiguity cascades into storage, analytics, and every system that touches the data. Use `required` to control presence and `"type": ["string", "null"]` to allow explicit null values. A field that is both not required and nullable can be absent (not applicable), present with a value, or present as `null` (asked but unknown): ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "middle_name": { "type": ["string", "null"] } }, "required": [], "additionalProperties": false } ``` See [Optional vs Null](/json-schema/optional-vs-null) for the full pattern. ## Add discriminators to unions Without a discriminator, your code has to guess which branch the model chose by looking at which fields are present. This works until two branches share a field name, and then it doesn't. A `const` discriminator makes the branch explicit: your runtime reads one field and knows exactly what it's dealing with. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }, { "type": "object", "properties": { "company": { "type": "string" } }, "required": ["company"] } ] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "kind": { "type": "string", "enum": ["person", "company"] }, "name": { "type": "string" }, "company": { "type": "string" } }, "required": ["kind"], "allOf": [ { "if": { "properties": { "kind": { "const": "person" } } }, "then": { "required": ["name"] } }, { "if": { "properties": { "kind": { "const": "company" } } }, "then": { "required": ["company"] } } ], "additionalProperties": false } ``` ## Encode field dependencies Requiring a `card_number` when the payment method is PayPal is noise. Omitting a `card_number` when it's card payment is a bug. If some fields only make sense together, or only make sense for certain values of another field, say so in the schema rather than leaving it to the model's judgment. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "payment_method": { "type": "string", "enum": ["card", "paypal"] }, "card_number": { "type": "string" }, "paypal_email": { "type": "string" } }, "required": ["payment_method", "card_number", "paypal_email"] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "payment_method": { "type": "string", "enum": ["card", "paypal"] }, "card_number": { "type": "string" }, "paypal_email": { "type": "string" } }, "required": ["payment_method"], "allOf": [ { "if": { "properties": { "payment_method": { "const": "card" } } }, "then": { "required": ["card_number"] } }, { "if": { "properties": { "payment_method": { "const": "paypal" } } }, "then": { "required": ["paypal_email"] } } ], "additionalProperties": false } ``` ## Compose independent rules with `allOf` When you have multiple independent conditions (one based on `channel`, another based on `priority`), flatten them into a single `if`/`then` and the logic gets unreadable fast. `allOf` lets you keep each rule as a separate block that's easy to read, test, and extend independently. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "channel": { "type": "string", "enum": ["email", "sms"] }, "priority": { "type": "string", "enum": ["low", "high"] }, "email_subject": { "type": "string" }, "phone_number": { "type": "string" }, "escalation_reason": { "type": "string" } }, "required": ["channel", "priority"] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "channel": { "type": "string", "enum": ["email", "sms"] }, "priority": { "type": "string", "enum": ["low", "high"] }, "email_subject": { "type": "string" }, "phone_number": { "type": "string" }, "escalation_reason": { "type": "string" } }, "required": ["channel", "priority"], "allOf": [ { "if": { "properties": { "channel": { "const": "email" } } }, "then": { "required": ["email_subject"] } }, { "if": { "properties": { "channel": { "const": "sms" } } }, "then": { "required": ["phone_number"] } }, { "if": { "properties": { "priority": { "const": "high" } } }, "then": { "required": ["escalation_reason"] } } ], "additionalProperties": false } ``` Each rule in the `allOf` is self-contained. Adding a new condition means adding a new block, not touching the existing ones. ## Bound your strings and arrays An unbounded `"type": "string"` can return anything from one character to an essay. An unbounded `"type": "array"` can return zero items or two hundred. In both cases, the model produces reasonable output most of the time, and then once in a while it doesn't, and something downstream breaks. Set bounds based on what your system actually accepts. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "summary": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["summary", "tags"] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "summary": { "type": "string", "maxLength": 240 }, "tags": { "type": "array", "items": { "type": "string", "maxLength": 24 }, "maxItems": 8 } }, "required": ["summary", "tags"], "additionalProperties": false } ``` Where do the numbers come from? Your database column width, your UI's line limit, the maximum items your frontend renders. If you don't know the exact limit, pick a reasonable one: `maxLength: 240` is better than no limit, even if the real answer turns out to be 280. ## Use constraints, not descriptions If a field must be a valid email address, `"description": "Must be a valid business email"` is a suggestion the model might follow. `"pattern": "^[^@]+@[^@]+$"` is a constraint it cannot violate. Descriptions don't guarantee your output's format; constraints ensure the model will follow the schema's syntax. **Before:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "email": { "type": "string", "description": "Must be a valid business email" } }, "required": ["email"] } ``` **After:** ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" } }, "required": ["email"], "additionalProperties": false } ``` This applies everywhere: dates should be `format: "date"`, not `"description": "ISO 8601 date"`. Country codes should be `pattern: "^[A-Z]{2}$"`, not `"description": "Two-letter country code"`. Anything you can express as a constraint, express as a constraint. ## Send us your schema Every dottxt customer gets access to a shared Slack channel with our team. Send us your JSON Schema before you ship it, and we'll tell you what's fragile and what will break. For a full audit, see [Schema Review](/audit). # Optional Fields Source: https://docs.dottxt.ai/json-schema/optional-fields Use optional fields intentionally so required data stays strict while extras remain flexible. Not every field can always be extracted. Some information may be absent from the source text, or may only be available after a secondary enrichment step. Making all fields required forces the model to hallucinate values for missing data; making all fields optional means your downstream code can never trust that anything is present. The right approach is to split fields into a required core (fields your application cannot function without) and optional enrichments (fields that add value when present but don't block processing when absent). Both groups stay typed and bounded; optional does not mean unconstrained. ## Use case Lead qualification output where `lead_id`, `segment`, and `priority` are always needed for routing, but `company_size`, `tech_stack`, and `notes` are only available when the source data mentions them. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "lead_id": { "type": "string", "pattern": "^LEAD-[0-9]{4,10}$" }, "segment": { "type": "string", "enum": ["smb", "mid_market", "enterprise"] }, "priority": { "type": "string", "enum": ["low", "medium", "high"] }, "company_size": { "type": "string", "enum": ["1-10", "11-50", "51-200", "201+"] }, "tech_stack": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 40 }, "maxItems": 10 }, "notes": { "type": "string", "maxLength": 300 } }, "required": ["lead_id", "segment", "priority"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field TechItem = Annotated[str, Field(min_length=1, max_length=40)] class LeadPayload(BaseModel): model_config = ConfigDict(extra="forbid") lead_id: str = Field(..., pattern=r"^LEAD-[0-9]{4,10}$") segment: Literal["smb", "mid_market", "enterprise"] priority: Literal["low", "medium", "high"] company_size: Literal["1-10", "11-50", "51-200", "201+"] | None = None tech_stack: list[TechItem] | None = Field(None, max_length=10) notes: str | None = Field(None, max_length=300) ``` Pydantic note: using `None` defaults makes these fields optional and nullable in the emitted schema. If you need omission without `null`, author the JSON Schema directly or use a schema representation that distinguishes those cases more directly. ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const leadPayloadSchema = z.object({ lead_id: z.string().regex(/^LEAD-[0-9]{4,10}$/), segment: z.enum(["smb", "mid_market", "enterprise"]), priority: z.enum(["low", "medium", "high"]), company_size: z.enum(["1-10", "11-50", "51-200", "201+"]).optional(), tech_stack: z.array(z.string().min(1).max(40)).max(10).optional(), notes: z.string().max(300).optional(), }).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": "Qualify lead LEAD-9821: mid-market company, 51-200 employees, uses Salesforce and HubSpot, high priority for Q2 migration." }], "response_format": { "type": "json_schema", "json_schema": { "name": "lead_qualification", "schema": { "type": "object", "properties": { "lead_id": { "type": "string", "pattern": "^LEAD-[0-9]{4,10}$" }, "segment": { "type": "string", "enum": ["smb", "mid_market", "enterprise"] }, "priority": { "type": "string", "enum": ["low", "medium", "high"] }, "company_size": { "type": "string", "enum": ["1-10", "11-50", "51-200", "201+"] }, "tech_stack": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 40 }, "maxItems": 10 }, "notes": { "type": "string", "maxLength": 300 } }, "required": ["lead_id", "segment", "priority"], "additionalProperties": false } } } }' ``` ## Example outputs Core-only output: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "lead_id": "LEAD-9821", "segment": "mid_market", "priority": "high" } ``` Enriched output: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "lead_id": "LEAD-9821", "segment": "mid_market", "priority": "high", "company_size": "51-200", "tech_stack": ["salesforce", "hubspot"], "notes": "Team requested migration support in Q2." } ``` ## Why this works The `required` array in the JSON schema guarantees that `lead_id`, `segment`, and `priority` are always present, so your routing logic never hits a missing key. The optional fields (`company_size`, `tech_stack`, `notes`) are still fully typed with enums, bounds, and `maxItems`, so when they do appear, they conform to the same quality standards as the required fields. This also makes the schema forward-compatible. When you add a new enrichment field later, existing consumers continue working because they only depend on the required core. The new field appears in outputs that have the data, and is absent from those that don't. ## Related docs * [Object reference](/json-schema/reference/object) * [Optional vs Null cookbook](/json-schema/optional-vs-null) # Optional vs Null Source: https://docs.dottxt.ai/json-schema/optional-vs-null Model omission and explicit null as different, intentional states. Optional and nullable look similar but mean different things: * **Optional** (not in `required`): the key may be absent entirely. This means "we didn't ask" or "not applicable." * **Nullable** (`type: ["string", "null"]`): the key is always present, but the value may be `null`. This means "we asked, but the answer is unknown." The distinction matters for storage, analytics, and downstream logic. If you treat both as the same thing, you can't tell whether a field was never captured or was captured and found to be empty, and that ambiguity cascades into every system that touches the data. ## Use case CRM contact records where `nickname` is a nice-to-have that the model may or may not extract (optional), but `middle_name` should always be present in the output and set to `null` when the source text doesn't mention one. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "first_name": { "type": "string", "minLength": 1 }, "middle_name": { "type": ["string", "null"], "maxLength": 80 }, "last_name": { "type": "string", "minLength": 1 }, "nickname": { "type": "string", "maxLength": 80 } }, "required": ["first_name", "middle_name", "last_name"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Contact(BaseModel): model_config = ConfigDict(extra="forbid") first_name: str = Field(..., min_length=1) middle_name: str | None = Field(..., max_length=80) last_name: str = Field(..., min_length=1) nickname: str | None = Field(None, max_length=80) ``` Pydantic note: `middle_name: str | None = Field(...)` maps cleanly to a required nullable field. For `nickname`, the idiomatic `str | None = Field(None, ...)` form makes the field optional and nullable in emitted schema. If you need omission without `null`, author the JSON Schema directly or use a schema representation that distinguishes those cases more directly. ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const contactSchema = z.object({ first_name: z.string().min(1), middle_name: z.string().max(80).nullable(), last_name: z.string().min(1), nickname: z.string().max(80).optional(), }).strict(); ``` ## Example outputs Known middle name, no nickname: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "first_name": "Alice", "middle_name": "Marie", "last_name": "Johnson" } ``` Unknown middle name, nickname present: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "first_name": "Alice", "middle_name": null, "last_name": "Johnson", "nickname": "AJ" } ``` ## Why this works In the first example, `nickname` is absent. The model didn't extract one, and your application can skip rendering it entirely. In the second example, `middle_name` is explicitly `null`. The model looked for a middle name and didn't find one, so your UI can show "Unknown" instead of leaving a blank gap. This distinction is especially important for analytics and data pipelines. A `COUNT` of non-null `middle_name` values tells you how many contacts have known middle names. A `COUNT` of records where `nickname` exists tells you how many contacts provided one. Without the distinction, both queries return the same misleading number. ## Related docs * [Object reference](/json-schema/reference/object) * [Improve Your Schema](/json-schema/improve-your-schema) # Overview Source: https://docs.dottxt.ai/json-schema/overview Treat JSON Schema as a generation program, not a formatting hint. A good schema is a contract between you and the model. The tighter the contract, the less work your application code does. If your schema says `"enum": ["billing", "technical", "account"]`, your routing logic doesn't need a default case. If it says `"minItems": 1`, your code doesn't need an empty-array check. If it says `"pattern": "^[A-Z]{2}$"`, you get country codes, not country names. Most schemas we see in production are too loose. They define the structure but not the boundaries: no length limits on strings, no bounds on arrays, and no patterns on identifiers. The model fills in reasonable values most of the time, and then once in a thousand requests it produces a 4,000-character "summary" or an array with 200 items, and something downstream breaks. This section is about writing schemas that don't break. Start with [Improve Your Schema](/json-schema/improve-your-schema) if you have an existing schema, or pick a [domain example](#domain-examples) close to your use case. ## Authoring Create schemas from the tools you already use. Define models in Python and generate JSON Schema from them. Define schemas in TypeScript with runtime validation. Generate schemas from example JSON with quicktype. Infer a baseline schema from representative JSON instances. Turn domain knowledge into constraints that guide generation. ## Patterns The difference between a schema that works and one that breaks in production usually comes down to a few missing constraints. These patterns address the problems we see most often. Control length, format, and regex patterns on string fields. Set min/max item counts to prevent runaway generation. Truly optional fields that the model can omit entirely. Route output to different shapes based on a discriminator field. Use if/then/else and dependent keywords when requirements vary by context. Model trees, nested structures, and self-referencing types. Constrain reasoning-style outputs into structured, inspectable fields. ## Domain examples Complete schemas for real tasks, with the reasoning behind each constraint choice. Enums, confidence scores, and grounded evidence. Pull structured fields from invoices, receipts, and documents. Normalize messy user input into typed backend payloads. Map natural language to execution-ready API requests. Generate renderable form specs from product requirements. Structured marketing copy with length bounds and tone control. ## Reference String, number, integer, boolean, null, const, object, array. Combine and reuse schemas with `allOf`, `anyOf`, `oneOf`, `not`, `$ref`, and `$defs`. Model context-dependent requirements with `if`/`then`/`else` and dependent keywords. Core type references: [String](/json-schema/reference/string), [Number](/json-schema/reference/number), [Integer](/json-schema/reference/integer), [Boolean](/json-schema/reference/boolean), [Null](/json-schema/reference/null), [Const](/json-schema/reference/const), [Object](/json-schema/reference/object), [Array](/json-schema/reference/array). # Quicktype Source: https://docs.dottxt.ai/json-schema/quicktype Infer JSON Schema from sample JSON using Quicktype. [Quicktype](https://quicktype.io/) infers JSON Schema from sample JSON data. It works well when you have example outputs but no model or schema definition yet. Quicktype currently emits draft-06 JSON Schema. That reflects the tool's output format, not a recommendation to target draft-06 in new hand-written schemas. ## Install ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} npm install -g quicktype ``` ## Basic usage Given a sample JSON file: ```json sample.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "name": "Alice Johnson", "email": "alice@acme.com", "role": "Product Manager" } ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src sample.json --src-lang json --lang schema -o contact.schema.json ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/Sample", "definitions": { "Sample": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "role": { "type": "string" } }, "required": ["email", "name", "role"], "additionalProperties": false, "title": "Sample" } } } ``` Quicktype infers types from the values it sees and wraps everything in a `$ref` + `definitions` structure. The type name (`Sample`) is derived from the filename. Unlike Genson, Quicktype sets `additionalProperties: false` by default. ## Multiple samples Pass multiple samples to improve inference. Quicktype merges them and detects optional fields: a field missing from some samples becomes non-required: ```json sample1.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"name": "Alice", "email": "alice@acme.com", "phone": "+1-555-0100"} ``` ```json sample2.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"name": "Bob", "email": "bob@acme.com"} ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src sample1.json --src sample2.json --src-lang json --lang schema ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/TopLevel", "definitions": { "TopLevel": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "phone": { "type": "string" } }, "required": ["email", "name"], "additionalProperties": false, "title": "TopLevel" } } } ``` `phone` is correctly detected as optional since it only appears in one sample. When multiple source files are provided, the type is named `TopLevel` instead of being derived from a filename. ## Nested objects Quicktype extracts nested objects into separate definitions and references them via `$ref`: ```json nested.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "name": "Alice", "address": { "street": "123 Main St", "city": "Springfield", "country": "US" } } ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src nested.json --src-lang json --lang schema ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/Nested", "definitions": { "Nested": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "$ref": "#/definitions/Address" } }, "required": ["address", "name"], "additionalProperties": false, "title": "Nested" }, "Address": { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "country": { "type": "string" } }, "required": ["city", "country", "street"], "additionalProperties": false, "title": "Address" } } } ``` The key name `address` is PascalCased to `Address` for the definition. ## Arrays Quicktype infers item types from array contents: ```json arrays.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "question": "What is your favorite color?", "options": ["red", "green", "blue"], "tags": ["survey", "color"] } ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src arrays.json --src-lang json --lang schema ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/Arrays", "definitions": { "Arrays": { "type": "object", "properties": { "question": { "type": "string" }, "options": { "type": "array", "items": { "type": "string" } }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["options", "question", "tags"], "additionalProperties": false, "title": "Arrays" } } } ``` Quicktype does not add `minItems` or `maxItems`; add those manually. See [Bounded Arrays](/json-schema/bounded-arrays). ## Arrays of objects When arrays contain objects, Quicktype extracts the item schema into a separate definition, singularizing the field name: ```json array_objects.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "contacts": [ {"name": "Alice", "email": "alice@acme.com"}, {"name": "Bob", "email": "bob@acme.com"} ] } ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src array_objects.json --src-lang json --lang schema ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/ArrayObjects", "definitions": { "ArrayObjects": { "type": "object", "properties": { "contacts": { "type": "array", "items": { "$ref": "#/definitions/Contact" } } }, "required": ["contacts"], "additionalProperties": false, "title": "ArrayObjects" }, "Contact": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } }, "required": ["email", "name"], "additionalProperties": false, "title": "Contact" } } } ``` The field `contacts` produces a definition named `Contact` (singular). ## Type inference across samples When multiple samples expose different field sets, Quicktype correctly distinguishes required from optional and infers `integer` vs `number`: ```json mixed1.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"id": 1, "label": "first"} ``` ```json mixed2.json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} {"id": 2, "label": "second", "score": 0.95} ``` ```bash CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} quicktype --src mixed1.json --src mixed2.json --src-lang json --lang schema ``` ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$ref": "#/definitions/TopLevel", "definitions": { "TopLevel": { "type": "object", "properties": { "id": { "type": "integer" }, "label": { "type": "string" }, "score": { "type": "number" } }, "required": ["id", "label"], "additionalProperties": false, "title": "TopLevel" } } } ``` `id` (whole numbers) is typed as `integer` while `score` (decimal) is typed as `number`. `score` is optional since it only appears in one sample. ## Limitations Quicktype infers structure and types but does not add semantic constraints. The generated schema will not include: * `enum` values * `minLength`, `maxLength`, or `pattern` for strings * `minItems`, `maxItems` for arrays * `description` on fields or the schema It also uses draft-06 and wraps every type in `$ref` + `definitions` rather than the more modern `$defs`. The output is a starting point. Tighten it by adding constraints manually or follow [Improve your schema](/json-schema/improve-your-schema). # Recursion Source: https://docs.dottxt.ai/json-schema/recursion Model tree-like outputs where nodes contain children of the same shape. Some data is naturally tree-shaped: file systems, navigation menus, org charts, comment threads, document outlines. A recursive schema models this by letting a node's `children` field reference the same node definition, creating an arbitrarily nested structure from a single type. The key concern with recursive schemas is unbounded growth. In this pattern, `maxItems` on the `children` array does not limit nesting depth directly; it limits how many children each node can have, which keeps deep trees from exploding in size. If you need a hard depth limit, you need a different schema design. ## Use case Generating a documentation table of contents tree from markdown headings, where each section can contain subsections of the same shape. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "$defs": { "tocNode": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 120 }, "slug": { "type": "string", "pattern": "^[a-z0-9-]+$" }, "children": { "type": "array", "items": { "$ref": "#/$defs/tocNode" }, "maxItems": 20 } }, "required": ["title", "slug", "children"], "additionalProperties": false } }, "type": "object", "properties": { "root": { "$ref": "#/$defs/tocNode" } }, "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 TocNode(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(..., min_length=1, max_length=120) slug: str = Field(..., pattern=r"^[a-z0-9-]+$") children: list[TocNode] = Field(..., max_length=20) class TocPayload(BaseModel): model_config = ConfigDict(extra="forbid") root: TocNode TocPayload.model_rebuild() ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; type TocNode = { title: string; slug: string; children: TocNode[]; }; const tocNodeSchema: z.ZodType = z.object({ title: z.string().min(1).max(120), slug: z.string().regex(/^[a-z0-9-]+$/), children: z.array(z.lazy(() => tocNodeSchema)).max(20), }).strict(); const tocPayloadSchema = z.object({ root: tocNodeSchema }).strict(); ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "root": { "title": "Getting Started", "slug": "getting-started", "children": [ { "title": "Installation", "slug": "installation", "children": [] }, { "title": "Quickstart", "slug": "quickstart", "children": [ { "title": "First Request", "slug": "first-request", "children": [] } ] } ] } } ``` ## Why this works The `$ref: "#/$defs/tocNode"` inside the `children` array creates the recursion: each node can contain children of the same shape, to any depth. Every node at every level is validated against the same constraints: `title` is bounded, `slug` matches a URL-safe pattern, and `children` exists (even if empty). `maxItems: 20` on `children` prevents any single node from having too many children. It does not cap recursion depth, but it does cap branching at each level, which keeps the total tree size more manageable and rendering more predictable. Without this bound, the model might generate hundreds of leaf nodes under a single parent, overwhelming your UI or exceeding token limits. ## Related docs * [Composition reference](/json-schema/reference/composition) * [Bounded Arrays cookbook](/json-schema/bounded-arrays) # Array Source: https://docs.dottxt.ai/json-schema/reference/array Ordered lists with item schemas and length constraints. Use `"type": "array"` for ordered lists. ## Core array keywords | Keyword | What it does | | ----------------------- | ----------------------------------------- | | `prefixItems` | Positional schemas for tuple-style arrays | | `items` | Schema for each array element | | `minItems` / `maxItems` | Bounds array length | `contains`, `minContains`, `maxContains`, `uniqueItems` and `unevaluatedItems` are not supported right now. If you have a use case that requires them, reach out to us. ## Example ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "values": { "type": "array", "items": { "type": "number" }, "minItems": 1, "maxItems": 5 } }, "required": ["values"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated from pydantic import BaseModel, ConfigDict, Field class Payload(BaseModel): model_config = ConfigDict(extra="forbid") values: Annotated[list[float], Field(min_length=1, max_length=5)] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ values: z.array(z.number()).min(1).max(5), }) .strict(); ``` For capped payload size, always set `maxItems`. The default value of `minItems` is 0. ## `prefixItems` Use `prefixItems` for tuple-style arrays where position matters: ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "metric": { "type": "array", "prefixItems": [ { "type": "string", "enum": ["latency_ms", "error_count"] }, { "type": "number", "minimum": 0 } ], "items": false } }, "required": ["metric"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field class Payload(BaseModel): model_config = ConfigDict(extra="forbid") metric: tuple[Literal["latency_ms", "error_count"], Annotated[float, Field(ge=0)]] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ metric: z.tuple([z.enum(["latency_ms", "error_count"]), z.number().min(0)]), }) .strict(); ``` This example enforces a two-element array: a metric name followed by a numeric value. ## Related * [Object reference](/json-schema/reference/object) * [Bounded Arrays cookbook](/json-schema/bounded-arrays) # Boolean Source: https://docs.dottxt.ai/json-schema/reference/boolean True/false fields for flags and toggles. Use `"type": "boolean"` for true/false values. ## Example ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "is_active": { "type": "boolean" }, "email_verified": { "type": "boolean" } }, "required": ["is_active", "email_verified"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") is_active: bool email_verified: bool ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ is_active: z.boolean(), email_verified: z.boolean(), }) .strict(); ``` Values must be the literals `true` or `false`, not quoted strings ("true" is rejected). Truthy integers like 1 and 0 are not accepted. Booleans are best for explicit flags. If you need multiple states, use a string `enum` instead. ## Related * [String reference](/json-schema/reference/string) * [Object reference](/json-schema/reference/object) # Composition Source: https://docs.dottxt.ai/json-schema/reference/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. ```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(); ``` `$ref` support is for local references (for example `#/$defs/...`). ## `allOf` Combine constraints that must all apply. ```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(), ); ``` 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. ```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(), ]); ``` ## `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 } ``` 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: ```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 = z.object({ name: z.string(), children: z.array(z.lazy(() => nodeSchema)).max(10), }).strict(); const payloadSchema = z.object({ root: nodeSchema, }).strict(); ``` ## Related * [Reusability cookbook](/json-schema/reusability) * [Recursion cookbook](/json-schema/recursion) * [Const reference](/json-schema/reference/const) * [Conditionals reference](/json-schema/reference/conditionals) # Conditionals Source: https://docs.dottxt.ai/json-schema/reference/conditionals Conditional validation based on field values and presence. Use conditional keywords when required fields depend on other values. ## Supported keywords | Keyword | What it does | | ---------------------- | ------------------------------------------------ | | `if` / `then` / `else` | Apply different schemas based on a condition | | `dependentRequired` | Require companion fields when a field is present | ## `if` / `then` / `else` Validate one branch when a condition matches, and another branch otherwise. Pydantic and Zod examples are omitted for this example because there is no direct keyword mapping for JSON Schema `if`/`then`/`else`. ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "delivery_method": { "type": "string", "enum": ["shipping", "pickup"] }, "address": { "type": "string", "minLength": 10 }, "pickup_location": { "type": "string", "enum": ["warehouse-a", "warehouse-b"] } }, "required": ["delivery_method"], "if": { "properties": { "delivery_method": { "enum": ["shipping"] } }, "required": ["delivery_method"] }, "then": { "required": ["address"] }, "else": { "required": ["pickup_location"] }, "additionalProperties": false } ``` In the JSON Schema above, if the value of the property `delivery_method` is "shipping", then the property `address` is required. Otherwise, it's the property `pickup_location` that is required. ## Multiple independent conditions Use `allOf` with multiple `if`/`then` blocks when separate rules should all apply. ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "account_type": { "type": "string", "enum": ["business", "personal"] }, "country": { "type": "string", "enum": ["US", "CA"] }, "company_name": { "type": "string" }, "state": { "type": "string" } }, "required": ["account_type", "country"], "allOf": [ { "if": { "properties": { "account_type": { "enum": ["business"] } }, "required": ["account_type"] }, "then": { "required": ["company_name"] } }, { "if": { "properties": { "country": { "enum": ["US"] } }, "required": ["country"] }, "then": { "required": ["state"] } } ], "additionalProperties": false } ``` In the JSON Schema above, there are two different conditionals that both apply. If the property `account_type` is equal to "business" then the property `company_name` is required and, independently of it, if the property `country` is equal to "US", then the property `state` is required. Thus, if both conditions evaluate to `true`, then both properties `company_name` and `state` are required. ## `dependentRequired` Require companion fields when a field is present. ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "email_verified": { "type": "boolean" }, "phone": { "type": "string" }, "phone_verified": { "type": "boolean" } }, "dependentRequired": { "email": ["email_verified"], "phone": ["phone_verified"] }, "additionalProperties": false } ``` In the JSON Schema above, if the property `email` is present, then the property `email_verified` must also be present. Similarly, if the property `phone` is present, then the property `phone_verified` must also be present. Use `dependentRequired` for presence-based dependencies, and `if`/`then`/`else` for value-based dependencies. ## Related * [Composition reference](/json-schema/reference/composition) * [Field Dependencies cookbook](/json-schema/field-dependencies) * [Conditional Requirements cookbook](/json-schema/conditional-requirements) # Const Source: https://docs.dottxt.ai/json-schema/reference/const Constrain a field to a single fixed value. Use `const` to pin a field to a single allowed value. ## Basic usage ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "version": { "const": "v1" } }, "required": ["version"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") version: Literal["v1"] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ version: z.literal("v1"), }) .strict(); ``` ## Discriminator pattern `const` is commonly used to tag branches in `oneOf`/`anyOf` schemas. ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "oneOf": [ { "type": "object", "properties": { "action": { "const": "search" }, "query": { "type": "string" } }, "required": ["action", "query"], "additionalProperties": false }, { "type": "object", "properties": { "action": { "const": "lookup" }, "id": { "type": "integer" } }, "required": ["action", "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 SearchAction(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal["search"] query: str class LookupAction(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal["lookup"] id: int Action = Annotated[SearchAction | LookupAction, Field(discriminator="action")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const actionSchema = z.discriminatedUnion("action", [ z .object({ action: z.literal("search"), query: z.string(), }) .strict(), z .object({ action: z.literal("lookup"), id: z.number().int(), }) .strict(), ]); ``` ## Related * [Composition reference](/json-schema/reference/composition) * [Union of Objects cookbook](/json-schema/union-of-objects) # Enum Source: https://docs.dottxt.ai/json-schema/reference/enum Restrict a value to a fixed set of choices. Use `enum` to restrict a field to a list of allowed values. ## Basic usage ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "priority": { "enum": ["low", "medium", "high"] }, "status": { "enum": ["open", "in_progress", "closed"] } }, "required": ["priority", "status"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") priority: Literal["low", "medium", "high"] status: Literal["open", "in_progress", "closed"] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ priority: z.enum(["low", "medium", "high"]), status: z.enum(["open", "in_progress", "closed"]), }) .strict(); ``` Use enums for categories and statuses so outputs stay predictable. ## Mixed-type enums `enum` values are not restricted to strings: ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "score": { "enum": [1, 2, 3, "unknown", null] } }, "required": ["score"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Literal from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") score: Literal[1, 2, 3, "unknown"] | None ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ score: z.union([ z.literal(1), z.literal(2), z.literal(3), z.literal("unknown"), z.null(), ]), }) .strict(); ``` For single-value enums, use [const](/json-schema/reference/const) instead. ## Related * [Const reference](/json-schema/reference/const) * [String reference](/json-schema/reference/string) # Integer Source: https://docs.dottxt.ai/json-schema/reference/integer Whole-number fields and integer constraints. Use `"type": "integer"` when decimal values are not valid. ## Common constraints ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "count": { "type": "integer", "minimum": 0 }, "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, "even_id": { "type": "integer", "multipleOf": 2 } }, "required": ["count", "priority", "even_id"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Payload(BaseModel): model_config = ConfigDict(extra="forbid") count: int = Field(..., ge=0) priority: int = Field(..., ge=1, le=5) even_id: int = Field(..., multiple_of=2) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ count: z.number().int().min(0), priority: z.number().int().min(1).max(5), even_id: z.number().int().multipleOf(2), }) .strict(); ``` `integer` supports the same numeric bound keywords as `number` (`minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`). ## When to use * Use `integer` for counters, indexes, and IDs. * Use `number` when decimals are allowed. ## Related * [Number reference](/json-schema/reference/number) * [Object reference](/json-schema/reference/object) # Null Source: https://docs.dottxt.ai/json-schema/reference/null Require a field to be present and explicitly null. Use `"type": "null"` when a field must be present with a `null` value. ## Example ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "middle_name": { "type": "null" }, "deprecated_field": { "type": "null" } }, "required": ["middle_name", "deprecated_field"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") middle_name: None deprecated_field: None ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ middle_name: z.null(), deprecated_field: z.null(), }) .strict(); ``` ## `null` vs optional fields * `type: "null"`: field is present and value is `null`. * Optional field: field may be absent. * Optional field + `type: "null"`: field is either absent or present with null. If you want a field that can be either string or null, use a union type: ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ] } }, "required": ["value"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") value: str | None ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ value: z.string().nullable(), }) .strict(); ``` ## Related * [Optional vs Null cookbook](/json-schema/optional-vs-null) * [Object reference](/json-schema/reference/object) # Number Source: https://docs.dottxt.ai/json-schema/reference/number Numeric values with decimal support and numeric constraints. Use `"type": "number"` for numeric fields that may include decimals. ## Supported numeric keywords | Keyword | What it does | | --------------------------------------- | ----------------------------- | | `minimum` / `maximum` | Inclusive numeric bounds | | `exclusiveMinimum` / `exclusiveMaximum` | Strict numeric bounds | | `multipleOf` | Restricts values to multiples | ## Example ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "score": { "type": "number", "minimum": 0, "maximum": 100 }, "temperature": { "type": "number", "exclusiveMinimum": -273.15 }, "price": { "type": "number", "minimum": 0, "multipleOf": 0.01 } }, "required": ["score", "temperature", "price"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Payload(BaseModel): model_config = ConfigDict(extra="forbid") score: float = Field(..., ge=0, le=100) temperature: float = Field(..., gt=-273.15) price: float = Field(..., ge=0, multiple_of=0.01) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ score: z.number().min(0).max(100), temperature: z.number().gt(-273.15), price: z.number().min(0).multipleOf(0.01), }) .strict(); ``` Use `number` for measurements, probabilities, and currency-like values. ## Related * [Integer reference](/json-schema/reference/integer) * [Object reference](/json-schema/reference/object) # Object Source: https://docs.dottxt.ai/json-schema/reference/object Object schemas with constraints. Use `"type": "object"` for keyed JSON structures. ## Core object keywords | Keyword | What it does | | ---------------------- | -------------------------------------------- | | `properties` | Declares named fields and their schemas | | `patternProperties` | Applies schemas to keys matching a regex | | `required` | Lists fields that must be present | | `additionalProperties` | Controls unknown keys | | `minProperties` | Minimum number of properties | | `maxProperties` | Maximum number of properties | | `propertyNames` | Schema that every property name must satisfy | `unevaluatedProperties` is not supported right now. If you have a use case that requires it, reach out to us. ## Example ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "age": { "type": "integer", "minimum": 0 }, "email": { "type": "string", "format": "email" } }, "required": ["name", "email"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, EmailStr, Field class Payload(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(..., min_length=1) age: int = Field(0, ge=0) email: EmailStr ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ name: z.string().min(1), age: z.number().int().min(0).optional(), email: z.string().email(), }) .strict(); ``` Set `additionalProperties: false` when you want strict, predictable output shape. ## `patternProperties` Use regex-based key validation for dynamic maps: ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "patternProperties": { "^S_[A-Z0-9]+$": { "type": "string" }, "^I_[A-Z0-9]+$": { "type": "integer", "minimum": 0 } }, "additionalProperties": false } ``` Pydantic and Zod examples are omitted here because there is no direct keyword mapping for `patternProperties`. ## `title`, `description`, `default`, and `examples` Use annotation keywords to document intent and provide generation hints: ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "title": "Customer Profile", "description": "Customer profile returned by the extraction pipeline.", "default": { "name": "Unknown Customer", "email": "unknown@example.com" }, "examples": [ { "name": "Alice Johnson", "email": "alice@example.com" } ], "properties": { "name": { "type": "string", "description": "Full legal name." }, "email": { "type": "string", "format": "email", "examples": ["alice@example.com"] } }, "required": ["name", "email"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, EmailStr, Field class Payload(BaseModel): """Customer profile returned by the extraction pipeline.""" model_config = ConfigDict( extra="forbid", json_schema_extra={ "examples": [ { "name": "Alice Johnson", "email": "alice@example.com", } ] }, ) name: str = Field(description="Full legal name.") email: EmailStr = Field( json_schema_extra={"examples": ["alice@example.com"]} ) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ name: z.string().describe("Full legal name."), email: z.string().email(), }) .strict() .describe("Customer profile returned by the extraction pipeline."); ``` These annotation keywords are ignored by structured generation and do not constrain the output. However, they can influence generation if they are included in the prompt. ## Related * [Array reference](/json-schema/reference/array) * [Conditionals reference](/json-schema/reference/conditionals) * [Additional Properties cookbook](/json-schema/additional-properties) * [Optional Fields cookbook](/json-schema/optional-fields) # String Source: https://docs.dottxt.ai/json-schema/reference/string String fields with length, pattern, and format constraints. Use `"type": "string"` for free-form text fields. Then add constraints to control size, format, and allowed values. ## Supported keywords | Keyword | What it does | | ------------------------- | -------------------------------------- | | `type: "string"` | Declares a text field | | `minLength` / `maxLength` | Bounds text length | | `pattern` | Enforces a regex | | `format` | Semantic format check (`email`, `uri`) | ## Length constraints ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "username": { "type": "string", "minLength": 3, "maxLength": 120 } }, "required": ["username"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated from pydantic import BaseModel, ConfigDict, StringConstraints class Payload(BaseModel): model_config = ConfigDict(extra="forbid") username: Annotated[str, StringConstraints(min_length=3, max_length=120)] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ username: z.string().min(3).max(120), }) .strict(); ``` Use this to avoid empty outputs and cap long generations. The length bounds are inclusive. ## Pattern constraints ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "country_code": { "type": "string", "pattern": "^[A-Z]{2}$" } }, "required": ["country_code"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated from pydantic import BaseModel, ConfigDict, StringConstraints class Payload(BaseModel): model_config = ConfigDict(extra="forbid") country_code: Annotated[str, StringConstraints(pattern=r"^[A-Z]{2}$")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ country_code: z.string().regex(/^[A-Z]{2}$/), }) .strict(); ``` This matches exactly two uppercase letters (for example country codes). ## Format constraints ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "email": { "type": "string", "format": "email" }, "website": { "type": "string", "format": "uri" } }, "required": ["email", "website"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import AnyUrl, BaseModel, EmailStr, ConfigDict class Payload(BaseModel): model_config = ConfigDict(extra="forbid") email: EmailStr website: AnyUrl ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const payloadSchema = z .object({ email: z.string().email(), website: z.string().url(), }) .strict(); ``` Formats are named patterns that validate well-known string shapes. The following formats are currently supported: | Format | Description | Example of Accepted Value | | ----------------- | --------------------------------- | ---------------------------------------- | | `date-time` | RFC 3339 date-time | `"2026-03-23T14:30:00Z"` | | `date` | RFC 3339 date | `"2026-03-23"` | | `date-time-local` | Date-time without timezone | `"2026-03-23T14:30:00"` | | `time` | RFC 3339 time with timezone | `"14:30:00Z"` | | `time-local` | Time without timezone | `"14:30:00"` | | `duration` | ISO 8601 duration | `"P3Y6M4DT12H30M5S"` | | `unixtime` | Seconds since Unix epoch | `"1742739000"` | | `utc-millisec` | Milliseconds since Unix epoch | `"1742739000000"` | | `email` | RFC 5321 email address | `"alice@example.com"` | | `uuid` | RFC 9562 UUID | `"550e8400-e29b-41d4-a716-446655440000"` | | `uri` | RFC 3986 URI | `"https://example.com/path"` | | `uri-reference` | URI or relative reference | `"/path/to/resource"` | | `uri-template` | RFC 6570 URI template | `"https://example.com/{id}"` | | `url` | Full URL with scheme | `"https://example.com"` | | `hostname` | RFC 1123 hostname | `"example.com"` | | `ipv4` | IPv4 address | `"192.168.1.1"` | | `ipv6` | IPv6 address | `"::1"` | | `byte` | Base64-encoded string | `"SGVsbG8="` | | `double` | Double-precision float as string | `"3.141592653589793"` | | `double-int` | Integer stored as double | `"42"` | | `float` | Single-precision float as string | `"3.14"` | | `int8` | Integer in \[-128, 127] | `"42"` | | `int16` | Integer in \[-32768, 32767] | `"1000"` | | `int32` | Integer in \[-2³¹, 2³¹-1] | `"100000"` | | `int64` | Integer in \[-2⁶³, 2⁶³-1] | `"9999999999"` | | `uint8` | Integer in \[0, 255] | `"200"` | | `uint16` | Integer in \[0, 65535] | `"50000"` | | `uint32` | Integer in \[0, 2³²-1] | `"3000000000"` | | `uint64` | Integer in \[0, 2⁶⁴-1] | `"10000000000"` | | `decimal` | Arbitrary-precision decimal | `"99.95"` | | `decimal128` | IEEE 754 decimal128 | `"12345.6789012345"` | | `sf-binary` | RFC 8941 structured field binary | `":SGVsbG8=:"` | | `sf-boolean` | RFC 8941 structured field boolean | `"?1"` | | `sf-decimal` | RFC 8941 structured field decimal | `"3.14"` | | `sf-integer` | RFC 8941 structured field integer | `"42"` | | `sf-string` | RFC 8941 structured field string | `"\"hello\""` | | `sf-token` | RFC 8941 structured field token | `"abc"` | | `expose` | Any string (no validation) | `"anything"` | ## Related * [Object reference](/json-schema/reference/object) * [Composition reference](/json-schema/reference/composition) * [String Bounds cookbook](/json-schema/string-bounds) # Reusability Source: https://docs.dottxt.ai/json-schema/reusability Use `$defs` and `$ref` to keep repeated structures consistent across your schema. 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 ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "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 } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, Field class Address(BaseModel): model_config = ConfigDict(extra="forbid") line1: str = Field(..., min_length=1, max_length=120) line2: str | None = Field(None, max_length=120) city: str = Field(..., min_length=1, max_length=80) postal_code: str = Field(..., min_length=3, max_length=20) country: str = Field(..., pattern=r"^[A-Z]{2}$") class OrderPayload(BaseModel): model_config = ConfigDict(extra="forbid") order_id: str = Field(..., pattern=r"^ORD-[0-9]{4,10}$") billing_address: Address shipping_address: Address ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z, toJSONSchema } from "zod"; const addressSchema = z.object({ line1: z.string().min(1).max(120), line2: z.string().max(120).optional(), city: z.string().min(1).max(80), postal_code: z.string().min(3).max(20), country: z.string().regex(/^[A-Z]{2}$/), }).strict().meta({ id: "addressSchema" }); const orderPayloadSchema = z.object({ order_id: z.string().regex(/^ORD-[0-9]{4,10}$/), billing_address: addressSchema, shipping_address: addressSchema, }).strict(); console.log(JSON.stringify(toJSONSchema(orderPayloadSchema, { reused: "ref", }), null, 2)); ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "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. ## Related docs * [Composition reference](/json-schema/reference/composition) * [Object reference](/json-schema/reference/object) # JSON Patch Streaming Source: https://docs.dottxt.ai/json-schema/streaming Stream structured output field-by-field as JSON Patch events as the model generates them. The dottxt API can stream a schema-constrained response one field at a time, instead of returning a single complete JSON object at the end. Each field arrives as a JSON Patch `add` operation, so downstream work (routing, dispatching, UI updates) can begin the moment the relevant field lands. The model generates fields in schema order. Field order in your schema decides when each field becomes available; routing keys, classifications, and gates should come first. For a broader discussion of JSON Patch stremaing, see [The closing brace](https://blog.dottxt.ai/the-closing-brace). This page covers the Python SDK helper. For the underlying wire format (`stream: "patch"`, NDJSON, SSE), see [JSON Patch streaming on `/chat/completions`](/api/chat-completions#json-patch-streaming). ## Quickstart `AsyncDotTxt.stream(...)` yields `PatchEvent` objects as the model fills in your schema: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import asyncio from typing import Literal from pydantic import BaseModel from dottxt import AsyncDotTxt class SupportTicket(BaseModel): # Field order = arrival order. Put what unblocks downstream work first. intent: Literal["billing", "technical", "account"] urgency: Literal["low", "medium", "high", "critical"] reply: str async def main(): client = AsyncDotTxt() stream = client.stream( model="openai/gpt-oss-20b", response_format=SupportTicket, input="I was charged twice this month, please refund the duplicate.", ) async for event in stream: match event.field: case "intent": print(f"dispatching to {event.value} queue") case "urgency" if event.value == "critical": print("paging oncall") case "reply": print(f"reply: {event.value}") asyncio.run(main()) ``` The routing decision fires the moment `intent` arrives — typically tens of milliseconds into generation — while `reply` continues to stream. ## The `PatchEvent` object Each yielded event carries: * **`event.op`** — the raw RFC 6902 operation: `{"op": "add", "path": ..., "value": ...}`. * **`event.snapshot`** — an independent deep copy of the document built up to and including this op. Safe to stash; later events do not mutate earlier snapshots. * **`event.field`** — the JSON Pointer with the leading `/` stripped. Top-level keys read as `"intent"`, array items as `"steps/0"`, nested fields as `"address/city"`. * **`event.value`** — the op's value. The four properties give you a clean `match` site for the common case; `event.op` and `event.snapshot` are available when you want the raw patch or the partial document so far. ## Parameters `AsyncDotTxt.stream(...)` mirrors `generate(...)`: * `model` (`str`) — model identifier. * `input` (`str | list[dict]`) — prompt string or chat-message list. * `response_format` (`Any`) — any schema input accepted by `generate(...)`: Pydantic model, JSON Schema dict/string, TypedDict, dataclass, etc. * `temperature`, `max_tokens`, `seed`, `timeout` — optional. * `extra` (`dict | None`) — additional chat-completions body fields. ## Examples ### Print each field as it arrives The smallest possible patch-stream consumer: iterate, print. No buffering, no closing brace. ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import asyncio from typing import Literal from pydantic import BaseModel, Field from dottxt import AsyncDotTxt class Engineer(BaseModel): name: str = Field(max_length=32) role: Literal["backend", "frontend", "ml", "infra"] years_experience: int = Field(ge=0, le=50) favorite_languages: list[str] = Field(min_length=1, max_length=4) async def main(): client = AsyncDotTxt() stream = client.stream( model="openai/gpt-oss-20b", response_format=Engineer, input="Generate a profile for a senior backend engineer.", ) async for event in stream: # Skip the structural seed ops (root `{}`, array `[]`) that arrive # before their contents; print only populated leaf fields. if not event.value: continue print(f"{event.field:>24} = {event.value!r}") asyncio.run(main()) ``` ### Route on a classification field before the long field finishes Order the schema so the routing key (here, `intent`) comes before the long-form `reply`. The dispatch decision fires tens of milliseconds in; the reply lands seconds later. The elapsed-time prefix on the reply is the punchline — how much later the full message lands compared to when routing was already settled. ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import asyncio import time from typing import Literal from pydantic import BaseModel, Field from dottxt import AsyncDotTxt class SupportTicket(BaseModel): intent: Literal["billing", "technical", "account", "feedback"] urgency: Literal["low", "medium", "high", "critical"] reply: str = Field(max_length=400) async def route_to_billing(ticket_id): print(f" -> dispatched {ticket_id} to billing queue") async def route_to_technical(ticket_id): print(f" -> dispatched {ticket_id} to technical queue") async def page_oncall(ticket_id): print(f" -> paged oncall for {ticket_id}") async def main(): client = AsyncDotTxt() ticket_id = "TKT-8821" started = time.monotonic() stream = client.stream( model="openai/gpt-oss-20b", response_format=SupportTicket, input="I was charged twice this month, please refund the duplicate.", max_tokens=400, ) async for event in stream: match event.field: # Fire-and-forget: routing kicks off while /reply is still streaming. case "intent" if event.value == "billing": asyncio.create_task(route_to_billing(ticket_id)) case "intent" if event.value == "technical": asyncio.create_task(route_to_technical(ticket_id)) case "urgency" if event.value == "critical": asyncio.create_task(page_oncall(ticket_id)) case "reply": elapsed = int((time.monotonic() - started) * 1000) print(f"reply ({elapsed}ms): {event.value}") asyncio.run(main()) ``` ### Fan out work on each array item When the schema has a top-level array, each item streams in as a separate field (`steps/0`, `steps/1`, ...). Launch a coroutine the moment each one arrives, so step 0's work is already underway while step 1 is still being generated. Total wall-clock time tends to be roughly one research interval longer than generation, not the sum of all research times. ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import asyncio import time from typing import Any from pydantic import BaseModel, Field from dottxt import AsyncDotTxt class Plan(BaseModel): topic: str = Field(max_length=80) steps: list[str] = Field(min_length=3, max_length=5) async def research(index, step): started = time.monotonic() print(f" [step {index}] started: {step!r}") # Pretend to do real work. await asyncio.sleep(1.0 + 0.2 * index) elapsed_ms = int((time.monotonic() - started) * 1000) print(f" [step {index}] done in {elapsed_ms}ms") return {"step": step, "elapsed_ms": elapsed_ms} async def main(): client = AsyncDotTxt() tasks: list[asyncio.Task[dict[str, Any]]] = [] started = time.monotonic() stream = client.stream( model="openai/gpt-oss-20b", response_format=Plan, input=( "Plan three to five research steps to answer the question: " "'What are the trade-offs between RAG and fine-tuning for " "domain-specific assistants?'" ), max_tokens=400, ) async for event in stream: if event.field.startswith("steps/") and event.value: index = int(event.field.split("/", 1)[1]) tasks.append(asyncio.create_task(research(index, event.value))) results = await asyncio.gather(*tasks) total_ms = int((time.monotonic() - started) * 1000) sum_research_ms = sum(r["elapsed_ms"] for r in results) print(f"\nall {len(results)} steps researched in {total_ms}ms total") print( f"sum of per-step research times: {sum_research_ms}ms " f"(overlap saved {max(0, sum_research_ms - total_ms)}ms)" ) asyncio.run(main()) ``` ### Mid-stream human approval Order high-risk decisions ahead of their effects. The proposed `action` arrives before the `reply`; prompt the operator between the two. If they decline, the rest of the stream is still consumed, but the `reply` is never sent. ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import asyncio from typing import Literal from pydantic import BaseModel, Field from dottxt import AsyncDotTxt class AgentDecision(BaseModel): # ``action`` precedes ``reply`` so the operator can approve or reject # while the reply text is still streaming. action: Literal["answer_only", "open_ticket", "issue_refund", "delete_account"] reply: str = Field(max_length=300) HIGH_RISK_ACTIONS = {"issue_refund", "delete_account"} async def ask_human(question): answer = await asyncio.to_thread(input, f"{question} [y/N]: ") return answer.strip().lower() in {"y", "yes"} async def send_reply(reply): print(f"sent reply: {reply}") async def main(): client = AsyncDotTxt() approved = True proposed_action = None stream = client.stream( model="openai/gpt-oss-20b", response_format=AgentDecision, input="Please close my account permanently. I am leaving.", max_tokens=300, ) async for event in stream: match event.field: case "action": proposed_action = event.value if event.value in HIGH_RISK_ACTIONS: approved = await ask_human(f"Approve action '{event.value}'?") case "reply" if approved: await send_reply(event.value) case "reply": print(f"discarded reply (action '{proposed_action}' declined)") asyncio.run(main()) ``` ## The full object so far If you need the partial object (e.g. to log progress or hand a partial object to another service), use `event.snapshot`. Each snapshot is an independent deep copy, so events can be stashed without later ops mutating earlier views: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} async for event in stream: log.info("partial document: %s", event.snapshot) ``` # String Bounds Source: https://docs.dottxt.ai/json-schema/string-bounds Constrain text length and shape so outputs fit downstream systems. A bare `"type": "string"` accepts anything from a single character to an essay. In production, that string usually ends up in a database column, an API field, or a UI component, all of which have size limits. When the model generates a 2000-character summary for a field that feeds into a 120-character database column, you either truncate (losing information) or reject and retry (wasting time and money). String bounds (`minLength`, `maxLength`) and format constraints (`pattern`, `format`) solve this at generation time. The model sees the constraints and produces text that already fits, so your application doesn't need post-processing heuristics. ## Use case A support ticket triage payload sent to a ticketing API with strict limits: `title` must be 10-120 characters, `customer_email` must be a valid email, and `language` should be an ISO 639-1 two-letter code. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "title": { "type": "string", "minLength": 10, "maxLength": 120 }, "summary": { "type": "string", "minLength": 30, "maxLength": 500 }, "customer_email": { "type": "string", "format": "email" }, "language": { "type": "string", "pattern": "^[a-z]{2}$" } }, "required": ["title", "summary", "customer_email", "language"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, ConfigDict, EmailStr, Field class TicketPayload(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(..., min_length=10, max_length=120) summary: str = Field(..., min_length=30, max_length=500) customer_email: EmailStr language: str = Field(..., pattern=r"^[a-z]{2}$") ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const ticketSchema = z.object({ title: z.string().min(10).max(120), summary: z.string().min(30).max(500), customer_email: z.string().email(), language: z.string().regex(/^[a-z]{2}$/), }).strict(); ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "title": "Unable to reset account password from mobile app", "summary": "Customer reports reset link opens correctly, but submitting a new password returns a generic error on iOS 18. Reproduced twice with account-specific token.", "customer_email": "sam@northwind.io", "language": "en" } ``` ## Why this works The `minLength`/`maxLength` constraints on `title` and `summary` guarantee the output fits the ticketing API's field limits. Without them, you'd need to truncate or retry, both of which degrade quality or add latency. The `pattern` constraint on `language` is stricter than `maxLength` alone: it enforces exactly two lowercase letters, so you get `"en"` instead of `"English"` or `"eng"`. The `format: "email"` constraint on `customer_email` guides the model to produce well-formed addresses rather than bare names or partial strings. Together, these constraints replace prompt-level instructions ("keep the title short", "use ISO language codes") with enforceable rules that the model cannot violate. ## Related docs * [String reference](/json-schema/reference/string) * [Object reference](/json-schema/reference/object) # Success and Error Output Source: https://docs.dottxt.ai/json-schema/success-error-output Use a single response envelope that stays parseable in both success and failure paths. When a model can either succeed or fail at a task, you need both outcomes to be parseable by the same code. If success returns `{"carrier": "ups", ...}` and failure returns `{"error": "missing zip"}`, every consumer needs two parsers and two code paths. A response envelope solves this: wrap both outcomes in a discriminator union keyed by `status`, and require the matching payload in each branch. ## Use case You are extracting shipping options from user text. Sometimes extraction succeeds, sometimes required details are missing. Your API client should always parse the same top-level keys regardless of outcome. ## Schema pattern ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "anyOf": [ { "type": "object", "properties": { "status": { "type": "string", "const": "success" }, "data": { "type": "object", "properties": { "carrier": { "type": "string", "enum": ["ups", "fedex", "dhl"] }, "service": { "type": "string", "minLength": 1, "maxLength": 40 }, "estimated_days": { "type": "integer", "minimum": 1, "maximum": 30 } }, "required": ["carrier", "service", "estimated_days"], "additionalProperties": false } }, "required": ["status", "data"], "additionalProperties": false }, { "type": "object", "properties": { "status": { "type": "string", "const": "error" }, "error": { "type": "object", "properties": { "code": { "type": "string", "enum": ["missing_input", "ambiguous_request", "unsupported_region"] }, "message": { "type": "string", "minLength": 1, "maxLength": 200 }, "retryable": { "type": "boolean" } }, "required": ["code", "message", "retryable"], "additionalProperties": false } }, "required": ["status", "error"], "additionalProperties": false } ] } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field class ShippingData(BaseModel): model_config = ConfigDict(extra="forbid") carrier: Literal["ups", "fedex", "dhl"] service: str = Field(..., min_length=1, max_length=40) estimated_days: int = Field(..., ge=1, le=30) class ErrorData(BaseModel): model_config = ConfigDict(extra="forbid") code: Literal["missing_input", "ambiguous_request", "unsupported_region"] message: str = Field(..., min_length=1, max_length=200) retryable: bool class SuccessResponse(BaseModel): model_config = ConfigDict(extra="forbid") status: Literal["success"] data: ShippingData class ErrorResponse(BaseModel): model_config = ConfigDict(extra="forbid") status: Literal["error"] error: ErrorData Response = Annotated[SuccessResponse | ErrorResponse, Field(discriminator="status")] ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const responseSchema = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.object({ carrier: z.enum(["ups", "fedex", "dhl"]), service: z.string().min(1).max(40), estimated_days: z.number().int().min(1).max(30), }).strict(), }).strict(), z.object({ status: z.literal("error"), error: z.object({ code: z.enum(["missing_input", "ambiguous_request", "unsupported_region"]), message: z.string().min(1).max(200), retryable: z.boolean(), }).strict(), }).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": "Ship via UPS Ground to New York, estimated 4 days." }], "response_format": { "type": "json_schema", "json_schema": { "name": "shipping_response", "schema": { "anyOf": [ { "type": "object", "properties": { "status": { "type": "string", "const": "success" }, "data": { "type": "object", "properties": { "carrier": { "type": "string", "enum": ["ups", "fedex", "dhl"] }, "service": { "type": "string", "minLength": 1, "maxLength": 40 }, "estimated_days": { "type": "integer", "minimum": 1, "maximum": 30 } }, "required": ["carrier", "service", "estimated_days"], "additionalProperties": false } }, "required": ["status", "data"], "additionalProperties": false }, { "type": "object", "properties": { "status": { "type": "string", "const": "error" }, "error": { "type": "object", "properties": { "code": { "type": "string", "enum": ["missing_input", "ambiguous_request", "unsupported_region"] }, "message": { "type": "string", "minLength": 1, "maxLength": 200 }, "retryable": { "type": "boolean" } }, "required": ["code", "message", "retryable"], "additionalProperties": false } }, "required": ["status", "error"], "additionalProperties": false } ] } } } }' ``` ## Prompt snippet ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} If you can determine a shipping option, set status="success" and fill data. If key details are missing, set status="error" and fill error. Never return both data and error as empty. ``` ## Example outputs Success: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "status": "success", "data": { "carrier": "ups", "service": "Ground", "estimated_days": 4 } } ``` Error: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "status": "error", "error": { "code": "missing_input", "message": "Destination ZIP code is missing.", "retryable": true } } ``` ## Why this works Every consumer starts by reading `status` and branching on its value. Because each `anyOf` branch fixes `status` with `const`, there is no ambiguity about which fields are valid next. The `anyOf` union still enforces the right shape here because the two branches are mutually exclusive: `status: "success"` requires `data`, and `status: "error"` requires `error`. This means your application never receives a success response with missing data, or an error response without an error code. ## Related docs * [Object reference](/json-schema/reference/object) * [Conditionals reference](/json-schema/reference/conditionals) * [Composition reference](/json-schema/reference/composition) # UI Generation Source: https://docs.dottxt.ai/json-schema/ui-generation Generate deterministic UI specs from natural language using JSON Schema as the rendering contract. Generating UI from natural language is powerful but fragile. If the model outputs a component name your renderer doesn't support, a field name with spaces, or 50 form fields for a simple request, your frontend either crashes or renders garbage. A strict schema turns the model's output into a guaranteed-renderable form spec: known component types, valid field names, bounded counts. The key insight is that your schema should mirror what your renderer actually accepts. If your React form builder supports six component types, the schema's `component` enum should list exactly those six. If your layout engine supports single and two-column modes, the schema should enumerate those. The model generates within these bounds, and the output is always renderable. ## Goal Convert product requests into a form specification that a React frontend can render directly without validation or transformation. ## Recommended contract ```json JSON Schema theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "type": "object", "properties": { "title": { "type": "string", "minLength": 3, "maxLength": 80 }, "layout": { "type": "string", "enum": ["single_column", "two_column"] }, "fields": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "component": { "type": "string", "enum": ["text", "email", "number", "select", "textarea", "checkbox"] }, "required": { "type": "boolean" }, "placeholder": { "type": "string", "maxLength": 80 }, "options": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 40 }, "maxItems": 20 } }, "required": ["name", "label", "component", "required"], "additionalProperties": false, "allOf": [ { "if": { "properties": { "component": { "const": "select" } } }, "then": { "required": ["options"] } } ] } } }, "required": ["title", "layout", "fields"], "additionalProperties": false } ``` ```python Pydantic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field OptionLabel = Annotated[str, Field(min_length=1, max_length=40)] class BaseField(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(..., pattern=r"^[a-z][a-z0-9_]*$") label: str = Field(..., min_length=1, max_length=50) required: bool placeholder: str | None = Field(None, max_length=80) class SelectField(BaseField): component: Literal["select"] options: list[OptionLabel] = Field(..., max_length=20) class SimpleField(BaseField): component: Literal["text", "email", "number", "textarea", "checkbox"] FieldSpec = Annotated[SelectField | SimpleField, Field(discriminator="component")] class FormSpec(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(..., min_length=3, max_length=80) layout: Literal["single_column", "two_column"] fields: list[FieldSpec] = Field(..., min_length=1, max_length=20) ``` ```typescript Zod theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { z } from "zod"; const baseFieldSchema = z.object({ name: z.string().regex(/^[a-z][a-z0-9_]*$/), label: z.string().min(1).max(50), required: z.boolean(), placeholder: z.string().max(80).optional(), }); const formFieldSchema = z.discriminatedUnion("component", [ baseFieldSchema.extend({ component: z.literal("select"), options: z.array(z.string().min(1).max(40)).max(20), }).strict(), baseFieldSchema.extend({ component: z.literal("text") }).strict(), baseFieldSchema.extend({ component: z.literal("email") }).strict(), baseFieldSchema.extend({ component: z.literal("number") }).strict(), baseFieldSchema.extend({ component: z.literal("textarea") }).strict(), baseFieldSchema.extend({ component: z.literal("checkbox") }).strict(), ]); const formSpecSchema = z.object({ title: z.string().min(3).max(80), layout: z.enum(["single_column", "two_column"]), fields: z.array(formFieldSchema).min(1).max(20), }).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": "Build a lead capture form for B2B demo requests. Need company email, team size with options 1-10, 11-50, 51-200, and 201+, and whether they want migration help." }], "response_format": { "type": "json_schema", "json_schema": { "name": "form_spec", "schema": { "type": "object", "properties": { "title": { "type": "string", "minLength": 3, "maxLength": 80 }, "layout": { "type": "string", "enum": ["single_column", "two_column"] }, "fields": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, "label": { "type": "string", "minLength": 1, "maxLength": 50 }, "component": { "type": "string", "enum": ["text", "email", "number", "select", "textarea", "checkbox"] }, "required": { "type": "boolean" }, "placeholder": { "type": "string", "maxLength": 80 }, "options": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 40 }, "maxItems": 20 } }, "required": ["name", "label", "component", "required"], "additionalProperties": false, "allOf": [ { "if": { "properties": { "component": { "const": "select" } } }, "then": { "required": ["options"] } } ] } } }, "required": ["title", "layout", "fields"], "additionalProperties": false } } } }' ``` ## Prompt pattern ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} You generate UI form specs for a React renderer. Return JSON only and follow the schema exactly. Keep labels concise and names snake_case. Use select only when options are explicit in the request. ``` ## Example input ```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} Build a lead capture form for B2B demo requests. Need company email, team size with options 1-10, 11-50, 51-200, and 201+, and whether they want migration help. ``` ## Example output ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "title": "B2B Demo Request", "layout": "two_column", "fields": [ { "name": "company_email", "label": "Company Email", "component": "email", "required": true, "placeholder": "name@company.com" }, { "name": "team_size", "label": "Team Size", "component": "select", "required": true, "options": ["1-10", "11-50", "51-200", "201+"] }, { "name": "needs_migration_help", "label": "Need Migration Help", "component": "checkbox", "required": false } ] } ``` ## Implementation tips * **Component enum mirrors your renderer.** If your form builder gains a new component type (e.g., `date_picker`), add it to the schema enum. If the model suggests a type not in the enum, the schema rejects it at generation time. * **Conditional requirements prevent incomplete specs.** The `allOf` block requires `options` when `component` is `"select"`. Without this, the model might produce a select field with no options: valid JSON, but unrenderable. * **Bounds protect layout.** `maxItems: 20` on fields prevents the model from generating overwhelming forms. `maxLength` on labels and placeholders keeps text from breaking your CSS grid. * **Field name pattern keeps keys predictable.** The `"^[a-z][a-z0-9_]*$"` pattern on `name` enforces lowercase snake\_case field keys, which keeps form state and backend mappings consistent without extra normalization. ## Related docs * [Conditional requirements](/json-schema/conditional-requirements): require `options` only when `component` is `"select"`, and similar patterns * [Unions of objects](/json-schema/union-of-objects): discriminated unions for different component types * [Object reference](/json-schema/reference/object) | [Conditionals reference](/json-schema/reference/conditionals) | [String reference](/json-schema/reference/string) # Union of Objects Source: https://docs.dottxt.ai/json-schema/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 ```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 } ] } } } }' ``` ## 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) # Migrate from Other Providers Source: https://docs.dottxt.ai/migrate-from-other-providers Switch from other providers to dottxt with one stable request shape. If you're migrating from OpenAI, Anthropic, Gemini, or another provider, use one stable path to dottxt. ## Shared migration pattern 1. Point your client at dottxt (`baseURL` + dottxt API key). 2. Send raw JSON Schema in `response_format` (avoid helper methods like `parse` that rewrite schemas). This keeps the request shape explicit and avoids provider SDK transformations that can silently change optionality and constraints. ## OpenAI-compatible example: change two lines ```python Before (OpenAI) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from openai import OpenAI client = OpenAI() response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "role": {"type": "string"} }, "required": ["name", "email", "role"], "additionalProperties": False } } } ) import json contact = json.loads(response.choices[0].message.content) ``` ```python After (dottxt) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from openai import OpenAI client = OpenAI( base_url="https://api.dottxt.ai/v1", api_key="your-dottxt-api-key", ) response = client.chat.completions.create( model="openai/gpt-oss-20b", messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "role": {"type": "string"} }, "required": ["name", "email", "role"], "additionalProperties": False } } } ) import json contact = json.loads(response.choices[0].message.content) ``` Two changes: 1. Set `base_url` and `api_key` to point at dottxt 2. Swap the model name Everything else, including `response_format`, `messages`, and response parsing, stays the same. ## What changes when you switch With OpenAI Structured Outputs, `strict: true` comes with a restricted JSON Schema subset. All object fields must be listed in `required`, objects must set `additionalProperties: false`, and some schema shapes are rejected outright. With dottxt, your schema is used as-is: | What you write | OpenAI behavior | dottxt behavior | | -------------------------- | -------------------------- | --------------------------- | | A field not in `required` | Not allowed in strict mode | Field is genuinely optional | | `minLength: 3` on a string | Supported | Enforced during generation | | `pattern: "^[A-Z]{2}$"` | Supported | Enforced during generation | | `minimum: 0` on a number | Supported | Enforced during generation | | `anyOf` at root level | Rejected | Supported | | `if` / `then` / `else` | Rejected | Supported | | Unsupported feature | Explicit error | Explicit error | See the [full provider comparison](/providers-comparison) for details. ## Code you can delete When your provider does not enforce the schema you actually want, you compensate with application code. Here's what that often looks like in practice: not the API call itself, but everything around it. ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} # Without full schema enforcement: the schema says minLength, pattern, optional fields... # but your application still has to clean up and validate the result itself import json from datetime import datetime response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": text}], response_format={"type": "json_schema", "json_schema": { ... }} ) result = json.loads(response.choices[0].message.content) # Validate what the schema was supposed to enforce if not result.get("vendor") or len(result["vendor"]) > 120: raise ValueError("vendor missing or too long") # Normalize date to match the contract your application expects raw_date = result.get("date", "") for fmt in ("%Y-%m-%d", "%B %d, %Y", "%m/%d/%Y", "%b %d %Y"): try: result["date"] = datetime.strptime(raw_date, fmt).date().isoformat() break except ValueError: continue # Normalize currency to the format your downstream code expects currency = result.get("currency", "").upper().strip() if currency in ("US DOLLARS", "USD$", "DOLLARS"): currency = "USD" result["currency"] = currency # Handle optional fields yourself when the response contract is not enforced directly if result.get("notes") in ("", "N/A", "None", "n/a"): result["notes"] = None # Retry if the output still doesn't conform if not validate(result): # try again, hope for better luck response = client.chat.completions.create(...) ``` With dottxt, the same task: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import json import os from openai import OpenAI from pydantic import BaseModel, Field class Invoice(BaseModel): vendor: str = Field(min_length=1, max_length=120) date: str = Field(json_schema_extra={"format": "date"}) currency: str = Field(pattern=r"^[A-Z]{3}$") line_items: list[dict] = Field(min_length=1, max_length=50) total: float = Field(ge=0) notes: str | None = Field(default=None, max_length=300) client = OpenAI( base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ) response = client.chat.completions.create( model="openai/gpt-oss-20b", messages=[{"role": "user", "content": text}], response_format={ "type": "json_schema", "json_schema": { "name": "invoice", "schema": Invoice.model_json_schema(), }, }, ) invoice = Invoice.model_validate_json(response.choices[0].message.content) # invoice.date is already "2026-02-12" # invoice.currency is already "USD" # invoice.notes is None when the source text doesn't contain any # no validation, no normalization, no retry ``` The validation code, the date normalization, the currency cleanup, the empty-string-to-None conversion, and the retry loop all exist because the schema wasn't enforced. Delete the workarounds, keep the schema. ## Richer schemas that now work Once you're on dottxt, you can use JSON Schema patterns that OpenAI's `strict` mode rejects: ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} response = client.chat.completions.create( model="openai/gpt-oss-20b", messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "role": { "type": "string" }, "tags": { "type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 5 } }, "required": ["name", "email"], "additionalProperties": false } } } ) ``` `name` is always non-empty. `email` matches the pattern. `tags` has 1–5 items. `role` is optional; it may or may not appear in the output. None of this works with OpenAI's `strict` mode. ## TypeScript The same approach works with any OpenAI-compatible TypeScript client: ```typescript Before (OpenAI) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import OpenAI from "openai"; const client = new OpenAI(); const response = await client.chat.completions.create({ model: "gpt-4o", messages: [ { role: "user", content: "Extract: John Smith , VP Engineering" } ], response_format: { type: "json_schema", json_schema: { name: "contact", strict: true, schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, role: { type: "string" } }, required: ["name", "email", "role"], additionalProperties: false } } } }); const contact = JSON.parse(response.choices[0].message.content!); ``` ```typescript After (dottxt) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.dottxt.ai/v1", apiKey: process.env.DOTTXT_API_KEY, }); const response = await client.chat.completions.create({ model: "openai/gpt-oss-20b", messages: [ { role: "user", content: "Extract: John Smith , VP Engineering" } ], response_format: { type: "json_schema", json_schema: { name: "contact", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, role: { type: "string" } }, required: ["name", "email"], additionalProperties: false } } } }); const contact = JSON.parse(response.choices[0].message.content!); ``` ```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": "Extract: John Smith , VP Engineering"} ], "response_format": { "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "role": {"type": "string"} }, "required": ["name", "email"], "additionalProperties": false } } } }' ``` # Provider Comparison Source: https://docs.dottxt.ai/providers-comparison JSON Schema support across structured output providers. Every structured output provider supports a different subset of JSON Schema. This page shows where they diverge so you can pick the right one for your schema. The table only includes features where at least one provider differs. Basic types, `items`, and `$defs` / `$ref` are supported by all four. ## String constraints | Feature | dottxt | OpenAI | Anthropic | Gemini | | ------------------------- | ------ | ------ | --------- | ------ | | `minLength` / `maxLength` | | | | | | `pattern` (regex) | | | | | | `format` | |

|

|

| ## Number constraints | Feature | dottxt | OpenAI | Anthropic | Gemini | | --------------------------------------- | ------ | ------ | --------- | ------ | | `minimum` / `maximum` | | | | | | `exclusiveMinimum` / `exclusiveMaximum` | | | | | | `multipleOf` | |

| | | ## Array constraints | Feature | dottxt | OpenAI | Anthropic | Gemini | | ----------------------- | ------ | ------ | --------- | ------ | | `prefixItems` (tuples) | | | | | | `minItems` / `maxItems` | | | | | | `contains` | | | | | | `uniqueItems` | | | | | ## Multiple choices | Feature | dottxt | OpenAI | Anthropic | Gemini | | ------- | ------ | ------ | --------- | ------ | | `enum` | | |

| | ## Object constraints | Feature | dottxt | OpenAI | Anthropic | Gemini | | ---------------------------------- | ------ | ------ | --------- | ------ | | Optional fields | | | | | | `additionalProperties` (as schema) | | | | | | `propertyNames` | | | | | | `patternProperties` | |

| | | | Arbitrary JSON | | | | | | `minProperties` / `maxProperties` | | | | | ## Composition | Feature | dottxt | OpenAI | Anthropic | Gemini | | ------- | ------ | ------ | --------- | ------ | | `anyOf` | |

| | | | `allOf` | | |

| | | `oneOf` | | | | | | `not` | | | | | ## Conditionals | Feature | dottxt | OpenAI | Anthropic | Gemini | | ---------------------- | ------ | ------ | --------- | ------ | | `if` / `then` / `else` | | | | | | `dependentRequired` | | | | | | `dependentSchemas` | | | | | ## Schema reuse and recursion | Feature | dottxt | OpenAI | Anthropic | Gemini | | ----------------- | ------ | ------ | --------- | ------ | | Recursive schemas | | | | | ## Schema limits | Limit | dottxt | OpenAI | Anthropic | Gemini | | --------------------- | ------ | --------- | --------------- | --------------- | | Nesting depth | None | 10 levels | None documented | None documented | | Total properties | None | 5000 | None documented | None documented | | Enum values | None | 1000 | None documented | None documented | | Optional parameters | None | N/A | 24 per request | None documented | | Union type parameters | None | N/A | 16 per request | None documented | ## Handling of unsupported features | Provider | Behavior | | ------------- | ----------------------------------------------------------------------------------------------------------- | | **dottxt** | Rejects the schema with an error specifying the unsupported construct | | **OpenAI** | With `strict: true`, unsupported schemas return an API error | | **Anthropic** | SDK strips unsupported constraints, adds them as text in field descriptions, validates response client-side | | **Gemini** | Silently ignores unsupported properties | ## Sources * [OpenAI documentation](https://developers.openai.com/api/docs/guides/structured-outputs/) * [Anthropic documentation](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) * [Gemini documentation](https://ai.google.dev/gemini-api/docs/structured-output) * Last verified: February 26, 2026 ## Next steps * [Supported JSON Schema features](/supported-features) * [Migrate from other providers](/migrate-from-other-providers) * [API overview](/api/overview) * [Integrations overview](/integrations/overview) # Quickstart Source: https://docs.dottxt.ai/quickstart Run your first structured-output extraction in minutes. Get from zero to your first validated JSON object in about five minutes. [Request API access here.](https://h1xbpbfsf0w.typeform.com/to/fwQNWmS8?typeform-source=docs.quickstart) ## dottxt CLI The fastest way to see structured output using the dottxt CLI: ```bash dottxt CLI theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install dottxt export DOTTXT_API_KEY="your-api-key" cat > contact.schema.json <<'JSON' { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "role": { "type": "string" } }, "required": ["name", "email"], "additionalProperties": false } JSON dottxt generate \ --model openai/gpt-oss-20b \ --schema contact.schema.json \ "Extract: John Smith , VP Engineering" ``` ```bash curl theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} export DOTTXT_API_KEY="your-api-key" 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": "Extract: John Smith , VP Engineering" }], "response_format": { "type": "json_schema", "json_schema": { "name": "contact", "schema": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "role": { "type": "string" } }, "required": ["name", "email"], "additionalProperties": false } } } }' ``` Ready to wire it into code? Follow the steps below. ## 1. Install ```shellscript Python (dottxt) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install dottxt ``` ```shellscript Python (OpenAI) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} pip install openai pydantic ``` ```shellscript TypeScript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} npm install ai @ai-sdk/openai ``` ## 2. Set your API key ```shellscript macOS/Linux theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} export DOTTXT_API_KEY="your-api-key" ``` ```powershell Windows (PowerShell) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} $env:DOTTXT_API_KEY="your-api-key" ``` ## 3. Run your first extraction ```python Python (dottxt) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} from pydantic import BaseModel, Field from dottxt import DotTxt class Contact(BaseModel): name: str = Field(min_length=1) email: str = Field(pattern=r"^[^@]+@[^@]+$") role: str | None = None client = DotTxt() result = client.generate( model="openai/gpt-oss-20b", input="Extract: John Smith , VP Engineering", response_format=Contact, ) print(result.model_dump()) ``` ```python Python (OpenAI) theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import os from openai import OpenAI from pydantic import BaseModel, Field class Contact(BaseModel): name: str = Field(min_length=1) email: str = Field(pattern=r"^[^@]+@[^@]+$") role: str | None = None client = OpenAI( base_url="https://api.dottxt.ai/v1", api_key=os.environ["DOTTXT_API_KEY"], ) response = client.chat.completions.create( model="openai/gpt-oss-20b", messages=[ {"role": "user", "content": "Extract: John Smith , VP Engineering"} ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": Contact.model_json_schema(), }, }, ) result = Contact.model_validate_json(response.choices[0].message.content) print(result.model_dump()) ``` ```typescript TypeScript theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} import { createOpenAI } from "@ai-sdk/openai"; import { generateObject, jsonSchema } from "ai"; const dottxt = createOpenAI({ baseURL: "https://api.dottxt.ai/v1", apiKey: process.env.DOTTXT_API_KEY!, }); const { object } = await generateObject({ model: dottxt.chat("openai/gpt-oss-20b"), schemaName: "contact", schema: jsonSchema({ type: "object", properties: { name: { type: "string", minLength: 1 }, email: { type: "string", pattern: "^[^@]+@[^@]+$" }, role: { type: "string" }, }, required: ["name", "email"], additionalProperties: false, }), prompt: "Extract: John Smith , VP Engineering", }); console.log(object); ``` Expected output: ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}} { "name": "John Smith", "email": "john@acme.com", "role": "VP Engineering" } ``` ## 4. Next steps * Learn schema patterns: [JSON Schema overview](/json-schema/overview) * Keep OpenAI-style code and switch providers: [Migrate from other providers](/migrate-from-other-providers) * Browse endpoints and auth: [API overview](/api/overview) * Send your schema for feedback before launch: [Schema audit](/audit) # Supported JSON Schema Features Source: https://docs.dottxt.ai/supported-features Quick reference for supported, unsupported, and partially supported JSON Schema keywords. You can use [JSON Schema](https://json-schema.org/) to define the structure of JSON objects returned by the model. We support most features from the JSON Schema [2020-12 specification](https://json-schema.org/draft/2020-12) across all of our products. We follow the specification unless otherwise noted. The request will fail if you pass a JSON Schema that contains keywords or combinations of keywords that are not supported. The error message will specify the unsupported construct. Don't hesitate to [reach out to us](mailto:contact@dottxt.ai) to request support. ## Quick reference | Keyword | Status | Notes | | -------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | ✓ | `string`, `number`, `integer`, `boolean`, `null`, `array`, `object` | | `minLength` / `maxLength` | ✓ | Strings; counts characters | | `pattern` | ✓ | Most regex features; see [unsupported patterns](#unsupported-regular-expressions) below | | `format` | ✓ | Strictly enforced at generation time. Supports: `email`, `hostname`, `ipv4`, `uri`, `uri-reference`, `uuid`, `date`, `time`, `date-time`, `duration` | | `enum` | ✓ | Works for any type | | `const` | ✓ | Works for any type | | `minimum` / `maximum` | ✓ | Numbers and integers | | `exclusiveMinimum` / `exclusiveMaximum` | ✓ | Numbers and integers | | `multipleOf` | ✓ | On integers | | `items` | ✓ | Homogeneous arrays (see note below) | | `prefixItems` | ✓ | Tuple validation (heterogeneous arrays) | | `minItems` / `maxItems` | ✓ | Array length | | `required` | ✓ | For object properties | | Optional fields | ✓ | | | `$defs` + `$ref` | ✓ | Internal refs only (no external refs) | | Recursive schemas | ✓ | Unlimited depth | | `anyOf` | ✓ | Including at the root | | `allOf` | ✓ | | | `oneOf` | ✓ | Treated as `anyOf` | | `additionalProperties` | ✓ | Defaults to `false` | | `{}` | ✓ | Arbitrary JSON | | `{"type": "object"}` | ✓ | Arbitrary object fields | | `propertyNames` | ✓ | | | `not` | ✓ | | | `if` / `then` / `else` | ✓ | | | `dependentRequired` | ✓ | | | `patternProperties` | ✓ | | | `minProperties` / `maxProperties` | ✓ | | | `contains` | ✓ | | | `mincontains` / `maxcontains` | - | [Request support](mailto:contact@dottxt.ai) | | `uniqueItems` | - | [Request support](mailto:contact@dottxt.ai) | | `dependentSchemas` | - | [Request support](mailto:contact@dottxt.ai) | | `unevaluatedProperties` / `unevaluatedItems` | - | [Request support](mailto:contact@dottxt.ai) | | `$dynamicRef` | - | [Request support](mailto:contact@dottxt.ai) | | `$dynamicAnchor` | - | [Request support](mailto:contact@dottxt.ai) | `items` can also be used for tuple validation along with `additionalItems` per the draft 2019-09 specification. We recommend using `prefixItems` for tuple validation and `items` for homogeneous arrays or extra items beyond the `prefixItems` tuple. ## Unsupported regular expressions The `pattern` keyword supports most common regex features. The following are not supported: * Word boundaries (`\b`) * Backreferences (`\1`, `(?P=open)`) * Conditional matches (`(?(1)a|b)`) * Lookaheads (`foo(?=bar)`) * Lookbehinds (`(?<=foo)bar`) * Atomic groups (`(?>pattern)`) * Recursion (`(?R)`, `(?1)`) * Named captures (`(?Ppattern)`) * Inline modifiers (`(?i)case-insensitive`) * Subroutines (`\g<1>`) * Branch resets (`(?|pattern1|pattern2)`) * Inline comments (`(?#comment)`) * Code callouts (`(*MARK:name)`) * Version checks (`(*VERSION)`) * Whitespace-insensitive patterns (`(?x)pattern # comment`) ## Next steps * [Compare provider support](/providers-comparison) * [Explore JSON Schema patterns](/json-schema/overview) * [Build a structured-output request](/api/chat-completions) * [Migrate from other providers](/migrate-from-other-providers)