mrkeyoor.com_
Fri 07 Aug 20:56 UTC
PyPIUtilsupdated 07 Aug 2026

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.

Verdict

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.

API stability4/5The four entry points repair_json, loads, load, and from_file have been stable for years and new capability arrives as keyword arguments with defaults, so upgrades rarely break call sites. It is still 0.x after 174 releases though, and the README asks you to pin json_repair==0.*, which is an honest admission that repair behaviour on odd inputs can shift between minor versions.
Docs4/5The README covers each flag with its tradeoff, names an antipattern users actually write, dumps the full CLI help, links runnable examples for FastAPI, Pydantic, and streaming, and is upfront that schema mode is beta. What is missing is a specification of what the heuristics will do to any given malformed input, so the only way to know how your data will be repaired is to test it.
Maintenance5/5Version 0.62.0 shipped 5 August 2026, the repository was pushed the same day, and there are 0 open issues and 0 open PRs against 5,065 stars. Development is test-driven with pre-commit and pytest wired up. The caveat the author states himself is that this is a side project run by one person and funded by sponsors.
Ecosystem4/5Around 8.89M weekly downloads and a common transitive dependency inside agent and extraction frameworks, with community ports to TypeScript, Go, Ruby, Rust, R, and Java. It is a leaf utility, so there are no plugins or integrations to speak of beyond the optional schema extra.

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
Skip it if

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:Payload

With 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

PackageRegistryPick it when
orjsonPyPIYour input is valid JSON and the actual requirement is throughput, not tolerance
dirtyjsonPyPIYour 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
instructorPyPIThe broken JSON comes from a model you control and you would rather retry with validation feedback than repair whatever came out
pydanticPyPIThe real need is coercing and validating a parsed object into a typed model, which you should probably do after repairing anyway