Skip to content
datahacks.dev
JSON

JSON Parse Error Single Quotes Instead of Double Quotes

JSON needs double quotes, but a Python repr or JavaScript literal uses single quotes. What each parser says, why replacing quotes breaks, and safe fixes.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the JSON logo.

The text looks like JSON. It has braces, colons, keys and values. The parser says it is not: Expecting property name enclosed in double quotes, or in Node, Expected property name or '}' in JSON at position 1. The reason is one character. JSON strings and property names are written with double quotes only, and the text has single ones. This is the single most common way for something that is almost JSON to arrive, because two languages that people use every day write data with single quotes by default: Python prints its dictionaries that way and JavaScript lets you write object literals that way. This article shows the messages, finds the source, explains why the quick fix of replacing the quotes goes wrong and gives the three repairs that work. It is one of the causes in fixing JSON parse errors by reading the message properly.

What the parsers say

The input {'a': 1} is rejected by every parser, and the position is 1, the opening single quote, the first character where a property name in double quotes was required:

Text
Python 3.13  JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
Node 24      SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
PHP 8.5      4 Syntax error
jq 1.6       parse error: Invalid numeric literal at line 1, column 5

Python's message names the double-quote rule outright, which makes it the easiest to recognize. Node's says what was expected, a property name or the end of the object, at the position of the quote. A single-quoted string value in an otherwise correct document, {"a": 'x'}, fails later, at the value, with Expecting value in Python. A top-level 'hello' fails at position 0, as in unexpected token in JSON at position 0.

If the position is 1 and the first character is { followed by a single quote, you have this problem. Look at the whole text: if True, False or None appears, it is a Python repr, and if keys have no quotes at all, it is probably a JavaScript literal.

Where it comes from

**str(dict), print(dict) or an f-string with a dict in Python.** The most frequent source. The built-in string form of a dictionary is its repr, and the repr uses single quotes around strings, the capitalized True, False and None, and no guarantee of anything else. A developer builds a request body with data = str(payload), or logs a dict and copies the line into a tool, and the receiving parser fails. Here is the same dictionary in both forms:

Text
str(dict):    {'name': 'Ada', 'active': True, 'manager': None, 'tags': ['a', 'b'], 'motto': "it's fine"}
json.dumps:   {"name": "Ada", "active": true, "manager": null, "tags": ["a", "b"], "motto": "it's fine"}

Note how many differences there are: the quotes, True against true, None against null, and the apostrophe string, which Python writes with double quotes because it contains a single one.

A JavaScript object literal. { name: 'Ada', tags: ['a', 'b'], } is valid JavaScript and invalid JSON in three ways: unquoted keys, single quotes and a trailing comma. It arrives from configuration files written by hand, from copying a value out of browser developer tools, from console.log output (Node prints objects in a single-quoted form) and from code that builds a string by hand.

Shell quoting. curl -d "{'a': 1}" URL sends single quotes because the shell passes the text unchanged, and the person meant to write the JSON in single quotes around the whole thing: curl -d '{"a": 1}' URL. On Windows cmd, single quotes do not quote at all, so the usual form needs double quotes with escaped inner ones, which is where people swap them by mistake.

Hand-edited and templated files, where an editor or a template engine swaps quote characters, and word processors, which produce curly quotes (a different problem with a similar message, covered in fixing JSON parse errors by reading the message properly).

Why replacing the quotes fails

The instinct is text.replace("'", '"'). On the repr above it produces:

Text
{"name": "Ada", "active": True, "manager": None, "tags": ["a", "b"], "motto": "it"s fine"}

and json.loads fails again, at character 26, Expecting value, because True is not a JSON literal. Two further problems hide behind that first one. The apostrophe in it's fine has become a double quote in the middle of a string, ending it early, so the string "it" is followed by the stray s fine". And a value that legitimately contains a double quote, written by Python as 'say "hi"', becomes "say "hi"". Replacement cannot tell a quote that delimits a string from one that is content, which is the whole reason strings have escapes. It works on trivial data and corrupts anything with an apostrophe, which in names and sentences is common: O'Brien, don't, it's. If you replace quotes in production code, it will eventually produce a document that is wrong in a way that looks right.

The three repairs that work

Fix the producer. If the text comes from your own code, write JSON with a serializer: json.dumps(payload) in Python, JSON.stringify(obj) in JavaScript, json_encode in PHP. This is the only fix that makes the problem disappear, and it is a one-line change. Search the codebase for str( and print( applied to dicts that cross a boundary, and for string concatenation that builds JSON.

**Parse a Python repr with ast.literal_eval.** If you must read text that is a Python dict repr, for example from a log or from a service that is not yours, ast.literal_eval parses Python literals, and only literals, without executing code (unlike eval, which would run anything in the text). On the sample it returned a dictionary equal to the original, True, None and the apostrophe string included, and json.dumps of that result gives valid JSON. Use it only on text you expect to be a literal, and catch its errors, since it raises ValueError or SyntaxError on anything else.

Parse a JavaScript literal with JSON5. For single quotes, unquoted keys and trailing commas, a JSON5 parser is the right tool. In Python, json5.loads("{'name': 'Ada', tags: ['a', 'b',], note: \"say 'hi'\"}") returned {'name': 'Ada', 'tags': ['a', 'b'], 'note': "say 'hi'"}, with the apostrophes inside the string intact. The same text through ast.literal_eval failed with a ValueError, because tags: without quotes is not a Python literal. In JavaScript, the json5 package does the same. Never use eval or new Function for this: they run whatever the text contains.

After either parse, write strict JSON with json.dumps and hand that downstream, so the fix happens once, at the edge (JSON versus JSON5 versus JSONC and when to use each covers where tolerant parsers belong).

Test the fix on the awkward strings

Whichever repair you choose, run it on the strings that break naive ones: an apostrophe (it's), a double quote inside a single-quoted string ('say "hi"'), a backslash, a newline, a Unicode character and the literals True, False and None as values and inside strings ('None of the above' must stay text). A repair that passes those is probably sound, and one that fails any should not be shipped. Keep the failing example as a regression test.

When quote replacement is acceptable

There is a narrow case where replacing is fine: a short, generated string you control completely, with a fixed set of keys and values that you know contain no quotes, apostrophes or backslashes, such as a hard-coded test fixture, converted once by hand. Even then, writing the correct JSON in the first place is as quick. For anything that comes from users, files or other services, use a parser or a serializer. The grammar that requires the double quotes is in RFC 8259, Python's literal_eval is documented in the ast module reference, and the JSON5 format at json5.org.

Also inDebugging
Taggederror-fix
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.