[BUG] Headless --json-schema: model serializes an array property as XML text inside a string property, then either retry-exhausts (error_max_structured_output_retries) or silently accepts a placeholder
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
When running headless (claude -p) with --output-format json --json-schema <schema>, for a task whose result is a moderately large structured object (a long string field plus an array of objects), the model reliably serializes the array as XML / tool-call-style text inside the first string property and never populates the array property. Every internal StructuredOutput attempt fails schema validation with must have required property '<array_field>', and the run ends either as subtype: "error_max_structured_output_retries" (strict schema) or subtype: "success" with a degenerate placeholder (loose schema) that's indistinguishable from a real result. (We've literally seen it output "placeholder" or "test" in order to pass the schema check)
Environment
Claude Code 2.1.204, headless (-p), --output-format json --json-schema, --max-turns 100, --permission-mode bypassPermissions, MCP servers attached
Auth: Claude subscription OAuth token
Reproduced on claude-opus-4-8 and claude-sonnet-5
What the transcript shows
The result object only contains the last attempt; the repeated failures are in the session transcript. In one run the model made 5 StructuredOutput calls, each with narrative (required string) = a correct ~5,000-char answer whose tail held the array serialized as XML (…</narrative>\n<findings>\n[ {…} ]\n</findings>\n</invoke>), and the required findings array absent — rejected each time with must have required property 'findings'. The content was correct; it was just placed in the wrong field. The generic error never tells the model the array must be a top-level property, so it repeats the mistake until retries exhaust.
Why it's easy to miss
When it degrades to a placeholder that passes a loose schema, the top-level result shows subtype:"success", is_error:false, terminal_reason:"completed", stop_reason:"tool_use" — no signal
Note, this is somewhat similar to related issues #70645/#76901/#74232 but this is the headless claude -p --json-schema path, whereas those are all the Workflow-tool agent({schema}) path
What Should Happen?
What should happen?
When --json-schema is supplied, the model's produced content should be captured faithfully into the schema fields. Specifically:
An array property (e.g. findings) should be populated as a top-level array, not serialized as XML/text inside a sibling string property. When the model does embed an array's content inside another string, the harness should recognize and correct it — ideally by re-prompting with a specific message (e.g. "emit findings as a top-level array, not inside narrative") instead of the generic must have required property error, which just produces identical retries.
If the model cannot produce schema-valid output after its retries, the run should fail detectably — is_error: true and/or a distinct error subtype — and never return subtype: "success" with a degenerate placeholder that a caller cannot distinguish from a real result.
A completed run reported as subtype: "success" should mean structured_output reflects the model's actual work, not a stub emitted purely to satisfy validation.
Error Messages/Logs
Steps to Reproduce
Save this generic schema as schema.json:
{
"type": "object",
"required": ["narrative", "findings"],
"additionalProperties": false,
"properties": {
"narrative": { "type": "string" },
"findings": {
"type": "array",
"items": {
"type": "object",
"required": ["summary", "details"],
"additionalProperties": false,
"properties": {
"summary": { "type": "string" },
"details": { "type": "string" }
}
}
}
}
}
Write a prompt that asks the model to analyze a moderately large input (e.g. a large multi-file diff or a long document) and return a long narrative plus several findings objects. The bigger the intended output, the more reliably it reproduces.
Run headless with schema-constrained output:
claude -p "<prompt>" --model claude-opus-4-8 --output-format json --json-schema schema.json --max-turns 100 > out.json
Inspect the result:
jq '{subtype, is_error, terminal_reason, structured_output}' out.json
The run ends either as subtype: "error_max_structured_output_retries" (strict schema), or subtype: "success" whose structured_output is a degenerate placeholder (loose schema).
(Optional) Confirm the root cause in the session transcript at ~/.claude/projects/<cwd>/<session_id>.jsonl (session id is .session_id from out.json):
grep '"name":"StructuredOutput"' <transcript>.jsonl
Each attempt's narrative string contains the findings serialized as XML (…</narrative>\n<findings>\n[ … ]\n</findings>\n</invoke>) while the top-level findings property is absent — so every attempt is rejected with must have required property 'findings'.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.204
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
iTerm2
Additional Information
Note, this seems to have been working _better_ until the week of July 6, that's when we really noticed it started failing consistently. No changes on our end.