Structured Output
Structured output constrains AI models to return valid JSON matching a schema you define. Instead of parsing free-form text, you get type-safe data that’s validated automatically.
When to Use Structured Output
- Data extraction — Pull entities from unstructured text
- Form generation — Create structured records from natural language
- API responses — Generate valid payloads for downstream services
- Classification — Get typed category/label responses
Requesting Structured Output
Set output_format on a Responses API request. With type: "json_schema", supply a name and a JSON Schema in json_schema.schema:
curl https://api.modelrelay.ai/api/v1/responses \
-H "X-ModelRelay-Api-Key: mr_sk_..." \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5-5",
"input": [
{"type": "message", "role": "user", "content": [{"type": "text", "text": "Extract: John Doe is 30 years old, email john@example.com"}]}
],
"output_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Full name"},
"age": {"type": "integer"},
"email": {"type": ["string", "null"]},
"tags": {"type": "array", "items": {"type": "string"}}
},
"required": ["name", "age", "email", "tags"]
}
}
}
}'
| Field | Description |
|---|---|
output_format.type |
json_schema, json_object, or text |
output_format.json_schema.name |
Name for the schema |
output_format.json_schema.description |
Optional description of the expected output |
output_format.json_schema.schema |
The JSON Schema the output must match |
output_format.json_schema.strict |
Optional strict-mode flag |
The JSON arrives as the text of the assistant message in output. Parse it from output[0].content[0].text:
{
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\",\"tags\":[]}"}
]
}
],
...
}
If the selected provider route cannot produce structured output, the request is rejected before it reaches the provider.
OpenAI- and Anthropic-compatible endpoints
POST /v1/chat/completions accepts the OpenAI response_format field, and POST /v1/responses accepts text.format, so existing OpenAI SDK code that requests JSON schema output works unchanged. See SDK compatibility.
Defining Schemas
Any library that emits JSON Schema works: write the schema by hand, or generate it from your language’s types (for example, Zod with z.toJSONSchema() in TypeScript, or Pydantic’s model_json_schema() in Python). Field names and description strings act as instructions to the model, so make them specific.
Model Compatibility Notes
The server normalizes every json_schema output format to the strictest provider requirements so the same schema works on every provider:
- Objects are closed. A missing or
trueadditionalPropertiesbecomesfalse. - Every property is required. The server rewrites
requiredto list every declared property, so represent optional fields as nullable (e.g.,string | null) rather than omitting them. - Map/dynamic objects (
map[string]any, unbounded keys) are not supported. AnadditionalPropertiessubschema returns400naming the schema path, for exampleoutput_format.json_schema.schema invalid at $.properties.tags.additionalProperties: must be false (maps are not supported).
The json_object output format skips schema normalization and only asks for valid JSON; not every provider supports it.
Streaming Structured Output
Structured output can be streamed with the same Accept headers as text (see Streaming). When output_format requests JSON, the stream runs in structured-patch mode instead of text-delta mode:
startreports"stream_mode": "structured-patch".updateevents carrypatch, a JSON Patch (RFC 6902) array to apply to the previous value, starting from{}, andcomplete_fields, the top-level fields whose values are complete.completioncarries the final object inpayload, along withcomplete_fields,usage, andstop_reason.
{"type":"start","stream_mode":"structured-patch","stream_version":"v2","request_id":"...","provider":"anthropic","model":"claude-sonnet-5-5"}
{"type":"update","patch":[{"op":"add","path":"/name","value":"John Doe"},{"op":"add","path":"/age","value":30}],"complete_fields":["name","age"]}
{"type":"completion","payload":{"name":"John Doe","age":30},"complete_fields":["name","age"],"stop_reason":"end_turn","usage":{...}}
ModelRelay emits a structured update once the model has produced a complete top-level JSON value; partial objects are not streamed field by field. If you only need the final object, read payload from the completion event. If the stream ends without a parseable JSON value, it ends with an error event whose code is INVALID_OUTPUT.
Validating the Result
ModelRelay normalizes your schema and asks the provider to constrain its output to it, but it does not re-validate the returned object against your schema. Parse and validate the JSON in your application before using it:
import { z } from "zod";
const Person = z.object({
name: z.string(),
age: z.number().int(),
email: z.string().nullable(),
tags: z.array(z.string()),
});
const text = response.output[0].content[0].text;
const person = Person.parse(JSON.parse(text));
Structured output can fail in two ways:
| Failure | Cause |
|---|---|
| Decode | The text is not valid JSON, for example because generation stopped at max_output_tokens |
| Validation | Valid JSON that does not match your schema |
To recover, check stop_reason first: a max_tokens stop means the output was truncated, so raise max_output_tokens. Otherwise you can send a follow-up request that appends the model’s previous output and a user message describing the error, asking it to return corrected JSON.
Complex Schemas
Structured output works with nested objects, enums, and arrays:
{
"type": "json_schema",
"json_schema": {
"name": "analysis",
"schema": {
"type": "object",
"properties": {
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
"confidence": {"type": "number"},
"topics": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"relevance": {"type": "number"}
},
"required": ["name", "relevance"]
}
},
"summary": {"type": ["string", "null"]}
},
"required": ["sentiment", "confidence", "topics", "summary"]
}
}
}
Best Practices
-
Use descriptive field names — The model uses field names as hints for what to generate
-
Add descriptions — Field descriptions help the model understand expected values
-
Keep schemas focused — Extract one concept at a time rather than requesting everything in one schema
-
Model optional fields as nullable — Every property becomes required, so use
["string", "null"]instead of leaving a field out ofrequired -
Leave room in
max_output_tokens— Truncated output is not valid JSON -
Validate before use — Decode errors (bad JSON) and validation errors (schema mismatch) need different handling
Next Steps
- Streaming — Text streaming basics
- Tool Use — Let models call functions
- Error Handling — Handle errors gracefully