Why json.loads fails on Python dict output from an LLM
Short answer: json.loads is a strict RFC 8259 parser and a model that emits {'a': True, 'b': None,} has not produced JSON at all - it has produced a Python literal. Of the 11 Python engines measured on this family of 100 cases, 9 recover none of the Python-literal cases at all. The measurements below come from a fixed, labelled corpus of 300 cases. It is EUR 29, one-time; the rest of this page is the answer, free.
The four ways output stops being JSON
Four categories of MALFORMED-300 cover the failure an engineer actually hits when a model is asked for JSON and answers in the dialect it was trained on: py_literals (True/False/None), single_quotes, unquoted_keys, and trailing_comma. Twenty-five cases each, 100 in total, each with the exact object the parser should have returned.
What each Python engine recovers
Exact match against the expected object - not 'did it raise', but 'is the value right'.
| engine | py_literals | single_quotes | unquoted_keys | trailing_comma | of 100 |
|---|---|---|---|---|---|
| jsonshim (Toolkit Labs) | 25/25 | 25/25 | 25/25 | 25/25 | 100/100 |
| json-repair | 21/25 | 25/25 | 25/25 | 22/25 | 93/100 |
| demjson3 | 0/25 | 16/25 | 25/25 | 20/25 | 61/100 |
| dirtyjson | 0/25 | 16/25 | 25/25 | 20/25 | 61/100 |
| json5 | 0/25 | 16/25 | 25/25 | 20/25 | 61/100 |
| pyjson5 | 0/25 | 16/25 | 25/25 | 20/25 | 61/100 |
| hjson | 0/25 | 16/25 | 22/25 | 19/25 | 57/100 |
| commentjson | 0/25 | 0/25 | 0/25 | 20/25 | 20/100 |
| json.loads (stdlib control) | 0/25 | 0/25 | 0/25 | 0/25 | 0/100 |
| partial-json-parser | 0/25 | 0/25 | 0/25 | 0/25 | 0/100 |
| simplejson | 0/25 | 0/25 | 0/25 | 0/25 | 0/100 |
Reading the table
ast.literal_evalis the usual first idea, and thepy_literalscolumn is why it is not a general fix: the moment onetrueornullsurvives from the JSON side of the prompt, a pure-Python literal evaluator raises.- Several JSON5-family engines land on the same total, 61 of 100, because they share the same permissive grammar: they take unquoted keys and trailing commas but not Python's literals.
- A permissive parser that accepts a dialect is not the same thing as a repair layer that reconstructs a value. The two columns move independently.
What to do instead
- Try
json.loadsfirst. It is the fast path and it is right whenever the model behaved. - On failure, run one repair layer - not three in a chain, because the second layer will happily 'fix' the first layer's wrong guess.
- Validate the recovered object against the schema you expected before you use it. Every table on this site scores exact match for exactly that reason: a parser that returns something has not solved your problem.
- Log which category you hit. The distribution of failures from your own model is the only thing that tells you whether this table's ranking is the one that matters to you.
Where these numbers come from
Every number on this page was computed from MALFORMED-300, Single quotes, unquoted keys, True/False/None and trailing commas: 11 Python engines measured on 100 labelled cases, scored on exact match against the expected object. The paid corpus is EUR 29, one-time, no account; a free sample and the scorer are on the product page, public domain. Questions: hello@toolkitlabs.org.