json-repair
json-repair is a pure-Python parser that reads broken JSON and gives you back a Python object anyway. It exists because language models produce JSON that is nearly right: a trailing comma, a missing closing brace, single quotes instead of double, the word Sure! before the object, a code fence around it, or a response that got cut off mid-string. Rather than failing, this parser walks the text character by character against the JSON grammar and patches what it can: closing brackets it believes should be closed, quotes it believes are missing, unquoted keys, stray prose, and truncated values that become null or empty string. The public surface is small and mirrors the standard library on purpose: json_repair.loads replaces json.loads, json_repair.load replaces json.load, and repair_json returns a cleaned-up string instead of an object. By default it tries the standard library first and only falls back to the repair parser when strict parsing fails, so the fast path on valid input is just json.loads. There is also a CLI, a strict mode that raises instead of guessing, and a beta schema mode that repairs against a JSON Schema or a Pydantic v2 model.
The best available answer to a problem you should try not to have, and the default fast path means it costs almost nothing to keep as a fallback. Treat every repair as a silent data change: log it, pin to the major version, and put schema validation behind it before the result reaches anything that matters.
Use it if
- You are parsing free-form model output and cannot enforce a schema at generation time, for example with an open-weights model behind an OpenAI-compatible endpoint that ignores response_format
- You are rendering partial JSON as it streams in and need each intermediate chunk to parse without flickering, which is what stream_stable is for
- You are cleaning a backlog of logs, scraped payloads, or user-pasted configuration where the input is already written and nobody is going to fix the producer
- You want a drop-in fallback that costs nothing on valid input: the default path calls json.loads first and only runs the repair parser on failure
- You need this in a constrained environment: no required runtime dependencies, pure Python, works on 3.10 and later, and installs in a second
- You can fix the producer instead. Structured outputs, JSON mode, tool calling, and constrained decoding all give you valid JSON at generation time, and a parser that guesses is strictly worse than a generator that cannot be wrong
- Silent wrongness is unacceptable. Repairing is guessing: a truncated response can become a structurally valid object with a field quietly set to null or an array quietly one element short, and nothing in the return value tells you that happened unless you pass logging=True. A dropped order line is worse than a raised exception
- Your JSON is valid and you want speed. This is a Python character loop, so on large valid payloads it is far slower than orjson, and forcing valid input through the repair parser with skip_json_loads=True can change the structure or values you get back
- You need schema-guaranteed output today. Schema-guided repair is labelled beta by the author, with bugs expected, so pydantic validation after parsing is the safer split of responsibilities
- You need a stable dependency contract. The project is on 0.62.0 after 174 releases with frequent updates, so the README tells you to pin as json_repair==0.* and accept that behaviour on edge-case inputs shifts between minor versions
- You need a real incremental parser. stream_stable stabilises the output of repeatedly reparsing an accumulating buffer, it does not parse a stream token by token, so cost grows with the square of the response length if you call it on every chunk
Setup reality
pip install json-repair gets you a package with zero required dependencies, which is most of the appeal. Schema-guided repair is behind an extra, so you need pip install 'json-repair[schema]' to pull in jsonschema and pydantic, and calling repair_json with schema= without that extra fails at import time rather than with a helpful message. Python 3.10 is the floor. The two names are easy to confuse: the distribution is json-repair with a hyphen, the import is json_repair with an underscore, and the CLI is json_repair too. The behaviour that surprises people is that repair_json returns a string by default and loads returns an object, so a lot of code ends up doing json.loads(repair_json(x)) when repair_json(x, return_objects=True) or plain loads(x) would do. The README calls out the other common mistake explicitly: wrapping the call in try json.loads except json_repair.loads is wasteful because the default path already tries json.loads first. Non-Latin text needs ensure_ascii=False or Chinese, Japanese, and Korean characters come back as escape sequences, and that flag only applies to the string-returning path. Two flags are mutually exclusive and raise ValueError rather than picking a winner: schema together with strict=True, and schema_repair_mode='salvage' without a schema. On completely unrepairable input repair_json returns an empty string rather than raising, so a bare truthiness check on the result is the difference between handling failure and silently processing nothing.
Patterns
Parse model output that may be brokenbasic-usage
import json_repair
bad = '{"users":[{"name":"Ada","role":"admin",}],"ok":true'
obj = json_repair.loads(bad)
# {'users': [{'name': 'Ada', 'role': 'admin'}], 'ok': True}loads already calls json.loads first and only falls back to the repair parser when that raises, so wrapping it in your own try/except around json.loads duplicates work the library does. Install name is json-repair with a hyphen, import name is json_repair with an underscore.
Get a cleaned string instead of an objectrepair-to-string
from json_repair import repair_json
fixed = repair_json(bad_json_string, indent=2)
# object instead, without a second parse
obj = repair_json(bad_json_string, return_objects=True)repair_json returns a string by default, so json.loads(repair_json(x)) is a round trip you do not need. Any keyword it does not recognise is passed through to json.dumps, which is how indent, sort_keys, and separators work. On input it cannot salvage at all it returns an empty string rather than raising.
Keep Chinese, Japanese, and Korean text readablenon-latin-characters
repair_json("{'test_chinese_ascii':'统一码'}")
# {"test_chinese_ascii": "\u7edf\u4e00\u7801"}
repair_json("{'test_chinese_ascii':'统一码'}", ensure_ascii=False)
# {"test_chinese_ascii": "统一码"}This is json.dumps behaviour passed straight through, so it only affects the string-returning path. With return_objects=True or loads() you already get real Python strings and the flag is irrelevant. It is also ignored when skip_json_loads is True.
Repair JSON on diskread-from-file
import json_repair
with open(fname, 'rb') as fd:
obj = json_repair.load(fd)
# or by path
obj = json_repair.from_file(json_file)Neither helper catches OSError or IOError, so file-not-found and permission problems are still yours to handle. Both read in chunks, which keeps memory flat on large files, but the repair parser still holds the decoded object in memory.
Skip json.loads when you know the input is brokenskip-validation-fast-path
from json_repair import repair_json
obj = repair_json(
known_bad_string,
return_objects=True,
skip_json_loads=True
)This is only faster when the input really is invalid, because the default path already short-circuits on valid input. Forcing valid JSON through the repair parser is not an equivalent mode: the parser may still restructure it, so a value you never wanted touched can change.
Validate instead of guessingstrict-mode
from json_repair import repair_json
try:
repair_json(payload, strict=True)
except ValueError as err:
print('rejected:', err)Strict mode raises ValueError on duplicate keys, missing colon separators, empty keys or values created by stray commas, multiple top-level elements, and similar ambiguity, while still tolerating the things it can resolve without guessing. Use it where a wrong-but-parseable object would be worse than a failure. It cannot be combined with schema.
Find out what was changedlog-the-repairs
from json_repair import repair_json
obj, repair_log = repair_json(
bad_json_string,
return_objects=True,
logging=True
)
if repair_log:
metrics.increment('json.repaired', len(repair_log))With logging=True the return value becomes a tuple, which breaks any caller expecting a single value. The log is empty when no repair was needed, so it doubles as a cheap signal for how often your upstream producer is emitting broken JSON. Log this in production or repairs are invisible.
Repair against a JSON Schemaschema-guided-repair
# pip install 'json-repair[schema]'
from json_repair import repair_json
schema = {
'type': 'object',
'properties': {'value': {'type': 'integer'}},
'required': ['value'],
}
repair_json('{"value": "1"}', schema=schema, return_objects=True)
# {'value': 1}Schema mode fills missing required fields, coerces safe scalars such as "1" to 1 and "yes" to True, and drops properties the schema disallows. It applies to valid input too, not just broken input, and it raises ValueError when the result still does not satisfy the schema. The author labels it beta.
Use a Pydantic v2 model as the schemapydantic-schema
from pydantic import BaseModel, Field
from json_repair import repair_json
class Payload(BaseModel):
value: int
tags: list[str] = Field(default_factory=list)
repair_json(
'{"value": "1", "tags": }',
schema=Payload,
skip_json_loads=True,
return_objects=True,
)You get back a dict, not a model instance, so you still call Payload.model_validate() on the result if you want the typed object. Passing a model is only a way to describe the shape. The salvage mode, schema_repair_mode='salvage', adds heavier heuristics such as dropping unfixable array items, and raises ValueError if you set it without a schema.
Parse a response while it is still arrivingstreaming-output
buffer = ''
for chunk in stream:
buffer += chunk
partial = json_repair.loads(buffer, stream_stable=True)
render(partial)stream_stable keeps the repaired shape steady across chunks so the UI does not flicker as a half-written value changes meaning. It is not an incremental parser: every call reparses the whole buffer from the start, so on a long response this is quadratic work. Throttle it or reparse only every few chunks.
Keep orjson on the happy pathorjson-fallback
import orjson
import json_repair
try:
obj = orjson.loads(payload)
except orjson.JSONDecodeError:
obj = json_repair.loads(payload, skip_json_loads=True)json_repair deliberately never auto-detects a faster JSON library, so this wiring is yours. skip_json_loads=True in the fallback avoids a third parse attempt with the standard library after orjson already failed.
Fix a file from the shellcli-usage
pipx install json-repair
json_repair broken.json --indent 2 -o fixed.json
json_repair -i broken.json # rewrite in place
cat broken.json | json_repair --strict
json_repair data.json --schema-model app.models:PayloadWith no filename it reads stdin, which makes it usable in a pipe after curl. --schema-model takes module:ClassName and imports it, so the module has to be on PYTHONPATH and importing it runs its top-level code. Install with the schema extra if you plan to use either schema flag.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| orjson | PyPI | Your input is valid JSON and the actual requirement is throughput, not tolerance |
| dirtyjson | PyPI | Your input is JavaScript-flavoured rather than broken: unquoted keys, single quotes, comments, trailing commas, and you want a parser that accepts that dialect instead of guessing at damage |
| instructor | PyPI | The broken JSON comes from a model you control and you would rather retry with validation feedback than repair whatever came out |
| pydantic | PyPI | The real need is coercing and validating a parsed object into a typed model, which you should probably do after repairing anyway |