Claude Structured Outputs Refusal Handling: Stop Parsing HTTP 200 Refusals A developer detailed a robust pattern for handling Claude structured outputs, emphasizing that an HTTP 200 response does not guarantee valid JSON and that applications must inspect the response envelope's stop_reason before deserialization. The approach classifies refusals and truncations as distinct states, preventing misleading parsing failures. The developer also noted the current API uses output_config.format for JSON Schema and recommended normalizing enum casing without weakening validation. Claude structured outputs refusal handling belongs before domain deserialization. A successful HTTP exchange only says the API accepted and processed the request; it does not guarantee that the text block contains the JSON object my application expects. Claude can return an HTTP 200 response with stop reason: "refusal" , and a response stopped by max tokens can contain incomplete JSON. If I unwrap content 0 .text and immediately call JsonSerializer.Deserialize , I turn a documented response state into a misleading parsing failure. The safer boundary is small: inspect the response envelope, classify the stop reason, and deserialize only a completed structured result. For the current stable API, I put the JSON Schema under output config.format . This replaces the earlier beta output format request shape, and the beta header is no longer required. The official structured outputs guide https://platform.claude.com/docs/en/build-with-claude/structured-outputs documents the current request format and its exceptional cases. The relevant part of a request looks like this: { "output config": { "format": { "type": "json schema", "schema": { "type": "object", "properties": { "action": { "type": "string", "enum": "approve", "escalate" }, "reason": { "type": "string" } }, "required": "action", "reason" , "additionalProperties": false } } } } Structured outputs normally give me schema-compliant JSON, but I still treat the envelope as authoritative. A refusal is a valid API response and may not follow my output schema. A max tokens stop can cut the generated document short. The stop-reason guidance https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons explains why each reason needs deliberate handling instead of a blanket success path. There is one more subtlety: enum and const text can differ in letter casing. I do not weaken validation into “accept any string.” I normalize casing only while matching against the finite enum declared by my application. I model decoding as a result, not an exception-driven happy path. The decoder first parses the outer message envelope, reads stop reason , and rejects refusal or truncation. Only then does it pass the text block to JsonSerializer.Deserialize . using System.Text.Json; using System.Text.Json.Serialization; public enum DecodeStatus { Success, Refusal, Truncated, InvalidPayload } public enum ReviewAction { Approve, Escalate } public sealed record ReviewWire property: JsonPropertyName "action" string Action, property: JsonPropertyName "reason" string Reason ; public sealed record ReviewResult ReviewAction Action, string Reason ; public sealed record DecodeResult DecodeStatus Status, ReviewResult? Value ; public static DecodeResult Decode string messageJson { using var message = JsonDocument.Parse messageJson ; var root = message.RootElement; var stopReason = root.GetProperty "stop reason" .GetString ; // Gate on the response envelope before touching structured text. if stopReason == "refusal" return new DecodeStatus.Refusal, null ; if stopReason == "max tokens" return new DecodeStatus.Truncated, null ; if stopReason = "end turn" || TryGetTextBlock root, out var text return new DecodeStatus.InvalidPayload, null ; try { var wire = JsonSerializer.Deserialize