You Asked for JSON. You Got a Code Fence and a Trailing Comma
The model gave you JSON wrapped in a code fence, with a trailing comma, using single quotes, preceded by "Sure, here you go!" like it's greeting you at a hotel, and cut off mid-string because it hit the token limit mid-thought. Your parser throws. Your pipeline dies. You retry the whole expensive call and hope this time is different, which is not a strategy, it's a coping mechanism.
There are two honest fixes. Constrain the decoding so only valid tokens ever come out (the grammar approach behind tools like Outlines), or repair the almost-valid output after the fact. structstream does the second, plus the part everyone forgets: it checks the repaired value actually matches your schema, because valid-but-wrong JSON breaks your code exactly the same way invalid JSON does.
Every Fix Targets a Specific, Common Failure
• stripped_code_fence: handles the ```json wrapper and bare ``` fences models love to add.
• extracted_json_span: strips the "Here is your JSON:" prose models put before and after the object.
• quoted_bare_keys and single_to_double_quotes: fix the Python-flavored JSON models produce when they are half-remembering a dict literal.
• closed_open_brackets and closed_open_string: recover objects the model cut off because it ran out of tokens mid-response.
Repair Without a Schema Check Is Half a Fix
A model can hand you flawless JSON with the wrong keys or a string where you needed a number, and your code downstream breaks exactly the same way it would from invalid JSON. The bundled validator supports the common subset (typed properties, required keys, typed arrays) so the benchmark only counts a sample as recovered if it clears both bars: parses, and matches.
The Number
Raw json.loads parses 1 out of 15 realistic malformed samples, 7%. structstream's repair recovers all 15, 100%, and every one of those 15 failure types is something a real model actually produces, not a synthetic edge case.
The Corpus Never Tested the Failure That Matters Most
I went and checked what those 15 samples actually cover. Code fences, prose, trailing commas, single quotes, truncation, all real. Not one of them contains a Python-literal True, False, or None in place of JSON's true, false, null. That's not an edge case I'm inventing to pad a number, it's one of the most common real failures: a model trained on far more Python dict literals than JSON objects drifts into Python syntax, often while getting everything else about the structure exactly right.
Against a sample that has it, the original repairer recovers 0%. Not degraded, zero. There was no fix for it at all, because the benchmark that was supposed to catch gaps like this never once exercised it.
The fix walks the text tracking whether it's inside a double-quoted string, the same technique the bracket-closer already uses, and only replaces whole-word True, False, and None outside of strings. That distinction matters: a value like "True Detective" needs to stay a string, not become a boolean because its content happens to contain the word. 0% to 100% on eight adversarial samples, confirmed on a five-sample holdout evaluated exactly once, with zero regression on the original 15-sample corpus. The original repairer is untouched; the fix ships as an explicit `repair_json_v2`.
28 tests pass in CI across Python 3.9, 3.11, and 3.13. Pure stdlib, no eval anywhere near it.