Get valid JSON out of a model that keeps almost getting it right.
Raw
json.loadsparses 7% of realistic malformed model output. One repair pass recovers 100%, and checks it matches your schema. No retries, no dependencies:python -m structstream.eval.
You asked for JSON. The model gave you JSON wrapped in a ```json fence, with a trailing comma, using single quotes, preceded by "Sure, here you go!" like a customer service rep, 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 pray to a distribution you don't control.
There are two honest ways out. Constrain the decoding so only valid tokens are ever emitted (the grammar approach behind tools like Outlines), or repair the almost-valid output after the fact, which is cheaper and doesn't require rearchitecting your inference stack. structstream does the second, plus the part everyone conveniently forgets: it validates the repaired value against your schema, because valid-but-wrong JSON breaks your code exactly as hard as invalid JSON does, just with more confidence on the way down.
Pure stdlib. No model, no network, no dependencies, no eval() anywhere near it. Every repair is a few lines you can actually read.
python -m structstream.evaljson repair benchmark: 15 realistic malformed model outputs
raw json.loads 1/15 = 7% parse
structstream repair 15/15 = 100% parse AND match schema
recovered by repair 14 outputs that would have crashed your pipeline
Every one of those 15 is a failure a real model actually produces, not a
synthetic edge case I invented to pad the number. The single one raw
json.loads handles is the already-valid case, the model having one good
day. The other 14 are the daily-driver failures: code fences, trailing
commas, single quotes, prose around the object, truncation. structstream
turns all of them back into usable data, and confirms each result has the
right keys and types before it hands them back, instead of celebrating too
early.
git clone https://github.com/ahmeddoghri/structstream
cd structstream && pip install -e .
python examples/quickstart.pyfrom structstream.repair import repair_json
from structstream.schema import matches
schema = {
"type": "object",
"required": ["name", "age"],
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
}
messy = "Here you go:\n```json\n{name: 'Ada', age: 36,}\n```"
result = repair_json(messy)
print(result.ok) # True
print(result.value) # {"name": "Ada", "age": 36}
print(result.fixes_applied) # what it had to fix, so nothing is a black box
print(matches(result.value, schema)) # True: right shape, not just valid JSONEach repair targets a specific, common failure, applied in a safe order. Text that is already valid passes straight through untouched.
| Fix | Handles |
|---|---|
stripped_code_fence |
```json ... ``` and bare ``` wrappers |
extracted_json_span |
"Here is your JSON:" and other prose around the object |
quoted_bare_keys |
{name: 1} to {"name": 1} |
single_to_double_quotes |
{'a': 'b'} to {"a": "b"} |
removed_trailing_comma |
[1, 2, 3,] and {"a": 1,} |
closed_open_brackets |
objects and arrays cut off at the token limit |
closed_open_string |
a string the model never closed |
Repairing to valid JSON is only half the job. 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. The bundled validator supports the
common subset (typed object properties, required keys, typed arrays, scalars),
so repair_json plus matches gives you a value that is both parseable and the
right shape. The benchmark only counts a sample as recovered if it clears both
bars.
pip install pytest && pytest -q # 15 passingMIT © Ahmed Doghri