JSON requires double quotes because its grammar only has one string delimiter: the double quote character. Every key in an object is defined as a string, so keys need double quotes too. {'name': 'Sam'} and {name: "Sam"} are both invalid JSON, even though JavaScript and Python happily read similar text.
What the grammar says
RFC 8259, the JSON standard, says "A string begins and ends with quotation marks", and its grammar defines that quotation mark as %x22, which is the " character. The apostrophe isn't mentioned anywhere. For objects it's just as short: "A name is a string." No rule allows bare identifiers.
In a recent Node.js release:
JSON.parse("{'name': 'Sam'}")
// Expected property name or '}' in JSON at position 1 (line 1 column 2)
JSON.parse('{name: "Sam"}')
// Expected property name or '}' in JSON at position 1 (line 1 column 2)
Same message for both, and the position is the character right after {. The parser expected " there and found something else. Python 3.14 is more direct about it: Expecting property name enclosed in double quotes: line 1 column 2 (char 1).
Why keys are always quoted
In a talk transcribed as The JSON Saga, Douglas Crockford tells how the very first test message failed: "we had a reserved word in there." The key was do, and "It turns out do is a reserved word in JavaScript." JavaScript at the time required reserved words to be quoted in the key position, which he called "really a nuisance".
The fix could have been a list of reserved words in the JSON spec. Instead, as Carsten Bormann later relayed Crockford's account on es-discuss, "they decided to simply always quote, and that was that." Crockford ends that part of the talk with: "And that's why, to this day, keys are quoted in JSON." He also points out that Python requires quoted keys in its dictionaries, so the rule didn't make JSON stranger for Python users either.
Why double and not single quotes
I couldn't find Crockford or the RFC explaining why the double quote won over the apostrophe. The JSON Saga transcript doesn't discuss it.
The spec does show the payoff of picking one. Inside a string, you must escape the quotation mark, the backslash, and the control characters U+0000 through U+001F. With two delimiters, a parser would track which one opened the string, and every writer would have two escaping rules to get right. An apostrophe inside a JSON string needs no escaping at all: "it's fine" is valid as written.
Where single quotes sneak in
Python's str() on a dict. This is the most common source I see. Printing or logging a dict gives Python syntax, not JSON:
d = {'name': 'Sam', 'active': True}
str(d) # {'name': 'Sam', 'active': True}
json.dumps(d) # {"name": "Sam", "active": true}
Single quotes and a capital True. Feed that to json.loads and you get "Expecting property name enclosed in double quotes". Call json.dumps wherever the text is produced, never str().
Copying a JavaScript object literal. { name: 'Sam' } from your source code is valid JavaScript and invalid JSON twice over: bare key, single-quoted value. Run it through JSON.stringify in a console instead of fixing quotes by hand. It always writes double quotes.
Hand-edited config. In a .json file it's easy to leave a key bare or type the quote your JavaScript linter prefers. The parser reports the first bad character's position.
If you already have single-quoted text
If the text is a Python literal that someone saved instead of JSON, Python's ast.literal_eval can read it back: it accepts only literals such as strings, numbers, lists, dicts, booleans and None. Then write it out with json.dumps. The docs warn that it isn't free from attack. A relatively small input can exhaust memory or the C stack, so they don't recommend it for untrusted data.
Don't search-and-replace ' with ". It breaks on the first apostrophe inside a value, like 'it's'.
If people hand-write the file and keep tripping on quotes, JSON5 allows single-quoted strings and lets keys be an ECMAScript 5.1 IdentifierName, so {name: 'Sam'} is valid JSON5. It's a different format, though: the program reading it needs a JSON5 parser.
My take: the double-quote rule is right, and JSON5 isn't worth adopting just to avoid typing ". Quote errors almost always mean JSON is coming from hand edits or str() where a serializer belongs. Fix the producer.
The limitation: strict quoting makes JSON clumsy to write by hand. It was built for programs to generate and parse, and a config file someone edits every day pays that cost on every edit.
Finding the bad quote
For a long payload, paste it into the OnlineViewer JSON validator. It shows the JSON.parse error with its position, and it runs in the page, so payloads with tokens in them don't leave your machine.
Quoting is one of three strict rules people hit most. The other two are trailing commas and comments, and in objects both can fail with a similar "property name" message. An empty response body gives a different error, covered in what "Unexpected end of JSON input" means.
FAQ
Can JSON use single quotes?
No. RFC 8259 defines a string as text that begins and ends with the double quote character, so 'text' in single quotes is a syntax error in any strict parser.
Why do JSON keys have to be quoted?
Early JSON left identifier-like keys bare, until an application used do as a key. do is a reserved word in JavaScript, so decoding broke, and the rule became: always quote keys.
Why does Python's json.loads reject my dictionary string?
str() on a Python dict gives Python syntax, with single quotes and True instead of true. Use json.dumps to produce JSON instead.
Is there a JSON variant that allows single quotes?
Yes. JSON5 allows single-quoted strings and unquoted keys that are valid ECMAScript 5.1 identifier names. It is meant for hand-written files, not data exchange.