Solving tips
- Always try a plain json.loads first; only repair when it actually fails, so you never corrupt valid input.
- Keep each repair small, ordered, and documented — trailing commas, then quotes, then balancing brackets.
- Return None on failure instead of raising, so the calling agent can retry or ask the model to regenerate.
Language models are the most common source of “almost JSON” — a tool call or structured response that is 99% valid but has a trailing comma, single quotes, or a missing closing brace. When you parse that output in an agent, a strict json.loads throws and the whole turn fails. A small, well-scoped repair pass rescues most of these cases without a heavyweight parser.
Task
Complete repair_json(text):
- First try
json.loads(text) unchanged. If it parses, return the result immediately — never repair valid input.
- If it fails, apply a short, fixed sequence of common repairs and retry after each one (or all together, then retry):
- Strip trailing commas that appear right before a
} or ] (e.g. {"a": 1,} -> {"a": 1}).
- Replace single quotes with double quotes (e.g.
{'a': 1} -> {"a": 1}).
- Balance brackets by appending the missing
} or ] characters when the string ends with more opens than closes.
- If the string still cannot be parsed after the repairs, return
None. Do not raise.
Keep the repairs simple and documented. This is a best-effort heuristic, not a full grammar.
Example
repair_json('{"a": 1, "b": 2}') # -> {"a": 1, "b": 2} (already valid)
repair_json('{"a": 1, "b": 2,}') # -> {"a": 1, "b": 2} (trailing comma)
repair_json("{'a': 1, 'b': 2}") # -> {"a": 1, "b": 2} (single quotes)
repair_json('{"a": 1, "b": [1, 2, 3') # -> {"a": 1, "b": [1, 2, 3]} (missing ] and })
repair_json('not json at all') # -> None
Constraints
- Return
None on unrecoverable input rather than raising.
- Do not use any third-party JSON-repair library; only the standard library (
json) is allowed.
- Never modify a string that already parses — check the happy path first.
- The repairs are heuristics: they only need to handle the documented cases, not every malformed string.
Approach
Try the strict parse first so valid input is never touched. On failure, apply a short, fixed pipeline of documented repairs — strip trailing commas, swap single quotes for double, and append the closing brackets needed to balance the string — then attempt the parse once more. Anything that still fails returns None instead of raising, so the caller can retry or regenerate.
Solution
import json
import re
from typing import Any, Optional
def repair_json(text: str) -> Optional[Any]:
if not isinstance(text, str):
return None
# Happy path: never touch input that already parses.
try:
return json.loads(text)
except json.JSONDecodeError:
pass
repaired = text
# Repair 1: strip trailing commas before a closing } or ]
# e.g. {"a": 1,} -> {"a": 1}
repaired = re.sub(r",(\s*[}\]])", r"\1", repaired)
# Repair 2: replace single quotes with double quotes
# e.g. {'a': 1} -> {"a": 1}
# Simple and lossy: fine for the common model-output case where
# the payload contains no apostrophes inside string values.
repaired = repaired.replace("'", '"')
# Repair 3: balance brackets by appending the missing closers.
# Count opens vs closes for {} and [] and append what's short.
# (Ignores brackets inside strings — a deliberate simplification.)
opens = {"{": "}", "[": "]"}
stack = []
for ch in repaired:
if ch in opens:
stack.append(opens[ch])
elif ch in ("}", "]") and stack and stack[-1] == ch:
stack.pop()
repaired += "".join(reversed(stack))
# Retry once after all repairs; give up gracefully on failure.
try:
return json.loads(repaired)
except json.JSONDecodeError:
return None
Walkthrough
On the examples:
'{"a": 1, "b": 2}' parses on the first json.loads, so it returns {"a": 1, "b": 2} untouched.
'{"a": 1, "b": 2,}' fails first. Repair 1’s regex matches the , before } and removes it, giving {"a": 1, "b": 2}, which parses.
"{'a': 1, 'b': 2}" fails first. Repair 2 turns every ' into ", giving {"a": 1, "b": 2}, which parses.
'{"a": 1, "b": [1, 2, 3' fails first. Repairs 1 and 2 are no-ops. Repair 3 walks the string: it pushes } for { and ] for [, and neither is ever popped, so stack == ["}", "]"]. Reversed and appended, the string becomes {"a": 1, "b": [1, 2, 3]}, which parses.
'not json at all' fails first, survives all three repairs unchanged (no commas, quotes, or brackets to fix), fails the retry, and returns None.
Complexity & notes
- Time is O(n) in the length of the string: the regex sub, the
replace, and the single bracket-balancing pass are each linear. Space is O(n) for the repaired copy and the bracket stack.
- Order matters. Trailing-comma stripping runs before bracket balancing so a
,} doesn’t get mistaken for an unbalanced structure, and quote replacement runs before the final parse.
- These repairs are intentionally naive. The single-quote swap is lossy if a string value contains an apostrophe (
"it's"), and the bracket counter does not skip brackets that appear inside string literals. That is an acceptable trade for a best-effort rescue; a production system would use a real tolerant parser or ask the model to regenerate.
- Returning
None rather than raising is the interview-relevant choice: in an agent loop it lets you feed the failure back to the model (“your JSON was invalid, try again”) instead of crashing the turn.
- The upfront
isinstance guard means a non-string input (e.g. None) degrades to None instead of blowing up in the regex step.