- Lock a tool-free Messages reply to a JSON Schema with
output_config.format, so parsers stop stripping markdown fences. - Keep the request free of tools. JSON outputs shape the text block. They do not stand in for a
tool_usecall. - Parse that text only when
stop_reasonisend_turn. Refusal and token cutoff drop the schema guarantee.
You asked Claude for a record and got a fenced blob, a missing key, or a string where the next service expects a boolean. “Return only JSON” is a wish, not a decoder. The Messages API can compile the schema into the sampler so the reply is already the object.
Why it matters
Record extraction breaks when Claude wraps JSON in prose, drops a required key, or changes a type. Asking for valid JSON in the prompt does not constrain the sampler. Structured outputs compile your schema into a grammar and sample only tokens that fit it. Put that schema on output_config.format with type set to json_schema. Do not attach a dummy tool, and do not set tool_choice. The object comes back in a text content block. The first request for a schema pays compile latency. Later calls reuse a grammar cached for 24 hours from last use. Keep additionalProperties false, list fields under required, and size max_tokens for the full object. Gate the parser on stop_reason before you trust the text.
How to force a JSON schema
A tool-free extraction call needs a model the structured outputs page lists, output_config.format set to json_schema, and no tools array. The snippet uses claude-opus-5, the model id in the current docs examples. Close every object, list required fields, and leave room in max_tokens for the whole object.
- 1. Define a closed object schema. Put every field under
propertiesand again underrequired. SetadditionalPropertiesto false on the root and on any nested object. - 2. Send the schema on
output_config.formatwithtypejson_schema. Do not send the beta headerstructured-outputs-2025-11-13, and do not putoutput_formatat the top level. - 3. Omit
toolsandtool_choice. If tools are present, Claude may emittool_usebefore any JSON text. - 4. After HTTP 200, branch on
stop_reason. Parse the text block only when it isend_turn.
#!/usr/bin/env bash
set -euo pipefail
: "${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY in your environment}"
curl -sS https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm."
}
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan_interest": {"type": "string"},
"demo_requested": {"type": "boolean"}
},
"required": ["name", "email", "plan_interest", "demo_requested"],
"additionalProperties": false
}
}
}
}'
Gotchas
- Unsupported schema keywords return 400:
minimum,maximum,multipleOf,minLength,maxLength, recursive schemas, external$ref, andadditionalPropertiesset to anything but false. Raw HTTP will not strip them. Some SDKs drop those constraints, append them to descriptions, and validate locally. stop_reasonofrefusalis still HTTP 200 and still billed. The text may ignore the schema.max_tokenscan cut the object mid-stream. Raise the cap and retry. Do not parse a partial JSON prefix.- String
enumandconstvalues are not case-guaranteed, often on the first letter after a space. Compare case-insensitively. Avoid labels that differ only by capitalization. - JSON outputs conflict with citations and with assistant message prefilling (400). An injected system prompt explains the format and adds input tokens. Changing
output_config.formatinvalidates prompt cache for that thread. - A new schema compiles on first use, then the grammar caches for 24 hours from last use. Do not put PHI in property names,
enum,const, orpattern. That cache is separate from message content. - Required keys come out first, then optional keys. If order matters, mark every property required.
- Python SDK 1.0 and later rejects
output_format={...}onclient.beta.messages.create()andcount_tokens().client.messages.parse()still acceptsoutput_formatand maps it tooutput_config.format.
Recommended AI prompt
Copy this paragraph into ChatGPT, Claude, Gemini, Grok, or whatever you use.
Review my Claude Messages extraction call and rewrite it so the reply is schema-locked JSON without tools. Put the schema on output_config.format with type json_schema, close every object with additionalProperties: false, list required fields, drop beta output_format headers, and add a stop_reason gate so I parse text only on end_turn. Return a short checklist plus the one request-body change.
Sources: Structured outputs · Messages API · Handling stop reasons