Claude returned ```json blocks 14% of the time. Here is the Rust crate I wish I had earlier. A Rust crate called `llm-json-repair` created to fix common formatting issues in JSON responses from Claude AI models. The author found that 14% of 12,400 structured-output API calls to Claude Sonnet 4.5 and 4.7 produced unparseable JSON, with the most common problems being code fences (9.3% of total) and trailing commas. The crate applies three sequential passes—stripping markdown code fences, extracting the first complete JSON object, and removing trailing commas—to repair responses locally without requiring additional LLM API calls. I had a system prompt that ended with: Reply with only a JSON object. Do not include code fences. Do not include explanation. That should be enough. It is not. Over a week of structured-output calls 12,400 of them, mostly Claude Sonnet 4.5 and 4.7 , I logged how often the response was actually parseable as JSON on the first serde json::from str attempt. Result: 86.0%. The other 14% had at least one of these problems: json ... wrappers most common, about 9.3% of total So I wrote llm-json-repair . Three passes, applied in order, each one cheap, none of them re-invoking the LLM. If a downstream model fix is needed, that is your problem to glue on. This crate is the local cleanup before you spend another API call. use llm json repair::repair; let raw = r " json { "intent": "book flight", "slots": { "origin": "DAL", "destination": "JFK", }, } " ; let cleaned = repair raw ?; let parsed: serde json::Value = serde json::from str &cleaned ?; What repair actually does, in order: Look for the first and the last . If both exist, take what is between them. The optional json language tag gets dropped along with the opening fence. If there is text after the closing fence, it is discarded. This pass is a one-liner conceptually, but the edge case that bit me was a JSON string value that contained the literal substring yes, a customer-support transcript got fed in once . So the matcher only strips fences at the outermost level and only if they are on their own line or at the start. Walk character by character. Find the first { or . Count nesting. Stop when nesting hits zero. Return that substring. This drops leading prose "Here is the JSON:" and trailing prose "Let me know if you need anything else." without needing a model. It also handles the case where the model emitted two JSON objects in a row rare but real ; you get the first complete one. rust let raw = "Sure thing Here is the JSON:\n{\"intent\": \"refund\", \"reason\": \"late\"}\nLet me know if you have questions."; let cleaned = repair raw ?; assert eq cleaned, r "{"intent": "refund", "reason": "late"}" ; The cost of this pass is O n over the response. For a 4 KB response it runs in about 30 microseconds on my laptop. Walk the candidate JSON. For each , followed only by whitespace and then } or , delete the comma. Skip when inside a string literal track quote state with escape handling . This is the single most common syntactic failure I see from Claude when it is generating a long object: it puts a trailing comma after the last field. It is a small thing but serde json rejects it. Fixing it locally saves you a round trip. Raw response: json json { "summary": "User wants to cancel order 4419", "actions": {"name": "cancel order", "args": {"id": 4419}}, {"name": "notify user", "args": {"channel": "email",}}, , } json Three problems: fences, two trailing commas, valid JSON otherwise. After repair : json { "summary": "User wants to cancel order 4419", "actions": {"name": "cancel order", "args": {"id": 4419}}, {"name": "notify user", "args": {"channel": "email"}} } Parses cleanly. No second LLM call. If all three passes run and the result still does not parse, repair returns an error. You should not silently swallow that. The crate exposes try repair which returns Result