Notes · Sep 5, 2026

Why JSON.parse rejects the dict you copied out of a Python log

The text looks like JSON, the parser says it is not, and both are right. A Python repr and a JSON document differ by exactly four rules — and by a few objects JSON cannot spell at all.

What print() actually writes

When a webhook handler logs its payload with print(event) or logger.info("%s", event), Python writes the object’s repr: dictionaries in braces, keys and strings in single quotes, and its own literals — True, False, None. pprint does the same with a hanging indent. That text is valid Python. It is not valid JSON, and json.loads() will refuse the first quote it meets.

It is also the text you end up holding. The process is gone, the object is gone, and the ticket, the log line or the terminal scrollback is what there is to work with.

stripe-event.pprint.txt
{'id': 'evt_1PZ3xK2eZvKYlo2C',
 'object': 'event',
 'api_version': '2024-06-20',
 'created': 1719491700,
 'livemode': False,
 'pending_webhooks': 1,
 'type': 'payment_intent.succeeded',
 'data': {'object': {'id': 'pi_3PZ3xJ2eZvKYlo2C0t1Z0QcT',
…

The four rules that differ

Everything else — braces, brackets, colons, commas, nesting, numbers — is the same. The differences are mechanical, which is why the fix can be too.

  • Strings and keys: Python prefers single quotes; JSON allows only double quotes. A double quote inside a single-quoted string has to be escaped on the way.
  • Booleans: True and False become true and false.
  • Null: None becomes null.
  • Trailing commas: Python tolerates one after the last element; JSON does not.

What has no JSON spelling

A tuple (1, 2), a set {1, 2}, datetime.datetime(2026, 4, 21) and Decimal("1.50") are Python objects with no JSON form. json.dumps() raises on them unless you pass a default; a repair of the text cannot invent one either. In unstringify a line that holds one stays on the problems list with its line number, and every other line is fixed around it.

Two ways to fix it

If you still have the process, use json.dumps(event) — or, for Stripe and most SDKs, the object’s own to_json() / to_dict() — and log that. It is the right fix and the only one that handles datetimes.

If you only have the text, paste it. The pprint sample above becomes valid JSON in 37 fixes, each named with its line — 32 × single → double quotes, 2 × python false → json false, 2 × python true → json true, 1 × python none → json null — and nothing else in the text is touched. The webhook sample on the fix page, printed the same way and broken in more ways, takes 46.

stripe-event.pprint.txt → JSON
{"id": "evt_1PZ3xK2eZvKYlo2C",
 "object": "event",
 "api_version": "2024-06-20",
 "created": 1719491700,
 "livemode": false,
 "pending_webhooks": 1,
 "type": "payment_intent.succeeded",
 "data": {"object": {"id": "pi_3PZ3xJ2eZvKYlo2C0t1Z0QcT",
…

Try it

Try it on the file that broke your afternoon.

free while in alpha · no signup