Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

Why search-and-replace is the wrong way to fix a trailing comma in JSON

A single comma can make a whole JSON file unusable: { "name": "ada", "roles": ["admin", "ops",], } # Python json.decoder.JSONDecodeError: Expecting value: line 3 column 28 (char 46) # Node / Chrome Synta

A single comma can make a whole JSON file unusable:

{
  "name": "ada",
  "roles": ["admin", "ops",],
}
# Python
json.decoder.JSONDecodeError: Expecting value: line 3 column 28 (char 46)

# Node / Chrome
SyntaxError: Unexpected token ']', ..."n", "ops",],
}" is not valid JSON

Both parsers stop at the first problem, the comma inside the array. Fix that one, and the comma before the final } fails next with Expecting property name enclosed in double quotes (Python) or Expected double-quoted property name in JSON (Node).

JSON (RFC 8259) has no trailing commas. JavaScript does, so these files usually come from config copied out of code, from templates that write item + ",", or from someone deleting the last key of an object and leaving the comma behind.

The one-liner everyone reaches for

re.sub(r",\s*([}\]])", r"\1", text)

It works on the example above. Now try it on data that contains those characters inside a string:

s = '{"note": "ends with ,}", "tags": ["a", "b",],}'
print(re.sub(r",\s*([}\]])", r"\1", s))
# {"note": "ends with }", "tags": ["a", "b"]}

The output parses, so nothing warns you, but the value of note has changed. That's worse than the original error, because now the corruption is silent.

Doing it properly: track whether you're inside a string

A comma can only be removed when it is outside a string and the next non-whitespace character closes an object or array. That means you have to know whether you're inside a string, and inside a string you have to watch for backslash escapes:

def remove_trailing_commas(text: str) -> str:
    out, in_string, escaped = [], False, False
    for i, ch in enumerate(text):
        if in_string:
            out.append(ch)
            if escaped:
                escaped = False
            elif ch == "\\":
                escaped = True
            elif ch == '"':
                in_string = False
            continue
        if ch == '"':
            in_string = True
        elif ch == ",":
            j = i + 1
            while j < len(text) and text[j].isspace():
                j += 1
            if j < len(text) and text[j] in "}]":
                continue  # drop the trailing comma
        out.append(ch)
    return "".join(out)
json.loads(remove_trailing_commas(s))
# {'note': 'ends with ,}', 'tags': ['a', 'b']}

Where to stop

It's tempting to keep going and "fix" single quotes, unquoted keys, comments, NaN, and missing brackets too. I'd argue you shouldn't. Each of those has more than one plausible repair, and a tool that silently picks one produces a file that parses but no longer means what it did. A trailing comma is different, because there is exactly one correct fix.

So the rule I use is to repair only what has a single defensible answer, and to fail loudly with a line and column for everything else.

Disclosure: I built a small free tool around this rule, JSON Repair. It removes trailing commas outside strings and strips a leading BOM. It refuses anything ambiguous. The full guide is here.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.