Paste eight characters into a browser console, {"a":1,}, and V8, the engine inside Chrome and Node, answers Expected double-quoted property name in JSON at position 7. Position 7 is the closing brace. The mistake is the comma at position 6, which the parser read, accepted and walked past, because a comma after a value is a legal promise that another key is coming. The brace broke the promise, so the brace takes the blame.
PHP does not go that far. json_decode() returns null and json_last_error_msg() hands you the string Syntax error. On PHP 8.3 I gave it four broken documents: a trailing comma, an unquoted key, an unterminated array, and a file beginning with a byte order mark. All four came back with the same two words. No line, no column, no character.
Both parsers also stop at the first fault, which is what costs time. A settings file with twelve small mistakes in it takes twelve rounds of fix, save, run, read, and each round names one character, usually not the one you typed wrong.
Why the message names the wrong character
A JSON parser is a state machine that reads left to right and never looks back. At each character it asks one question: can this continue a legal path from where I am standing? It has no model of what you meant. So the position in the error is not the position of the mistake, it is the first position at which the mistake became impossible to ignore.
That is why different mistakes produce identical messages. {a:1} and {'a':1} both give Expected property name or '}' in JSON at position 1: the parser had opened an object and needed either a double quote or a closing brace, and got neither. The message describes the parser’s expectation, not your intention, which it never had access to.
A JSON validator that halts at the first fault answers a question you did not ask. The tool below reads the document once with a hand written tolerant scanner that recovers at every fault instead of throwing, so twelve mistakes come back as twelve problems in one pass, each with a line, a column, the offending character and the state it was found in. It runs entirely in your browser: nothing is uploaded and the text never leaves the page.
JSON validator and repair
Paste JSON that will not parse. This says which line, which column and which character broke it, in a sentence rather than a code, and it keeps reading so you see every problem at once instead of one per attempt. Then it repairs what can safely be repaired, lists exactly what it changed, and formats, minifies, sorts, walks and diffs the result. Everything happens in this browser tab: nothing is uploaded, nothing is stored, nothing leaves the page.
Click a row to take its JSON Pointer. The pointer of the whole document is the empty string.
Switch one off and that kind of problem is still reported, but no output is offered until you switch it back on or fix it by hand. NaN and Infinity also covers undefined and Python's True, False and None. A few repairs are always applied because there is no second reading: a missing colon, a closing bracket that does not match the one that opened, a raw control character in a string, an invalid escape, a number written in a way JSON forbids, and a key that appears twice in one object.
The whole grammar, which is smaller than you think
JSON is unusual in that you can state all of it in a paragraph. RFC 8259 defines six structural characters: {, }, [, ], : and ,. It allows exactly four whitespace bytes between tokens: space (0x20), horizontal tab (0x09), line feed (0x0A) and carriage return (0x0D). It has three literal words, true, false and null, all lowercase. A value is an object, an array, a string, a number or one of those words, and a document is one value.
Strings open and close with the double quote character 0x22 and nothing else. Inside them eight escape sequences are defined (", , /, b, f, n, r, t) plus uXXXX, and every character below 0x20 must be escaped rather than typed. Numbers have a fixed shape: optional minus, then a single zero or a digit from 1 to 9 followed by more digits, then an optional fraction, then an optional exponent. That shape forbids a leading plus, a leading zero, a bare decimal point at either end, and hex.
Everything a parser has ever rejected in your files breaks one of those rules. Knowing the list is most of the diagnosis: it turns “this looks fine to me” into “which of six rules did I break”.
The four mistakes, and why each one is an error
The trailing comma is the most common and the least deserved. In JavaScript, Python, Rust and Go a comma after the last element is legal and encouraged, because it keeps diffs to one line when you add an item. JSON has no production for a comma followed by a closing brace or bracket. The comma separates two things, and if there is no second thing there is no separator.
The single quote fails because the string rule names one delimiter. An apostrophe cannot begin a value, cannot begin a key, and has no meaning outside a string. The unquoted key fails for the same reason: an object member is a string, then a colon, then a value, not an identifier then a colon, which is what JavaScript object literals allow and what most people’s fingers type. That difference is the biggest source of the belief that JSON and JavaScript are the same thing. They are close relatives, and every place they differ is a place where a file breaks.
Comments are absent on purpose. Douglas Crockford removed them after watching people use them to smuggle parsing directives into documents, which would have made JSON a format you interpret rather than read. So // and /* */ produce Unexpected token '/', and every configuration format built on JSON since has invented its own answer: a _comment key, a superset like JSON5, or a different format entirely.
What can be repaired, and what is always repaired
Nine repairs sit behind switches: trailing commas, missing commas, single quoted strings, typographic quotes, unquoted keys and values, comments, NaN and Infinity, the byte order mark, and unclosed brackets. Each is a decision with an obvious right answer, which is why it is safe to automate and why every change is still listed with its position.
Turning a switch off does not hide the problem. It is still reported, still highlighted in the input, and marked as left alone, but the output is withheld until you switch the repair back on or fix that spot by hand. That keeps the switches honest: the scanner has to recover internally to keep reading either way.
A short list is always applied, because there is no second reading available: a missing colon between key and value, a closing bracket that does not match the one that opened, a raw control character inside a string, an invalid escape, a number JSON forbids, and a key that appears twice in one object. On that last one, JSON does not forbid duplicate keys, it simply does not define what they mean, which is why json_decode() keeps the last and other parsers keep the first.
Loose words are handled as one group. NaN, Infinity, undefined and Python’s True, False and None become the JSON word JSON.stringify would have used. Valid numbers keep their original text rather than being converted and printed again, so a 19 digit identifier does not lose its last digits to a double. Key order is preserved exactly as found, including numeric looking keys, because the document is held in an ordered node model instead of a plain object.
The byte order mark, three bytes that break a perfect file
A byte order mark is the code point U+FEFF, written in UTF-8 as the three bytes EF BB BF. In UTF-16 it tells a reader which end of each pair comes first. In UTF-8 there are no pairs and no ambiguity, so it carries no information at all: it is a signature saying “this file is Unicode”, added by editors offering a “UTF-8 with BOM” option and by exporters that still assume Windows-1252 is the alternative.
It breaks JSON because a document starts with a value, and U+FEFF is neither whitespace nor a value. The failure is spectacularly unhelpful: V8 reports Unexpected token followed by a character with no width, then quotes your document back at you, where it looks exactly correct. PHP returns error code 4, Syntax error, which is what it returns for everything. RFC 8259 says implementations must not add a byte order mark and may ignore one they receive, so the same file is valid for some parsers and invalid for others, and neither is a bug.
# is there a BOM on this file?
head -c 3 config.json | xxd
# 00000000: efbb bf ...
file config.json
# config.json: Unicode text, UTF-8 (with BOM) text
# strip it from the first line only
sed -i '1s/^xEFxBBxBF//' config.json
The same three bytes cause a second problem in WordPress. A PHP file saved with a BOM emits them before <?php is even reached, which counts as output, which means headers have already been sent by the time anything tries to set one. The symptom is a headers already sent warning from a file where you can see nothing wrong.
Quotes a word processor changed behind your back
Copy a snippet through Word, Google Docs, Notion or any editor with smart quotes enabled and the straight quote 0x22 is replaced by a typographic pair: U+201C and U+201D for doubles (E2 80 9C and E2 80 9D in UTF-8), U+2018 and U+2019 for singles. The result reads better and parses worse.
Curly quotes only break JSON when they are doing a quote’s job. Inside a properly double quoted string they are ordinary UTF-8 text and entirely legal. I checked both in PHP: typographic quotes around a key is a syntax error, typographic quotes inside a string value returns error code 0, no error. So “curly quotes break JSON” is half true, and the half matters when you are hunting through a long file.
The nastier passenger from the same journey is U+00A0, the non breaking space, two bytes C2 A0. It is invisible, looks exactly like a space in every editor, and is not one of JSON’s four whitespace bytes. Put one before a colon and V8 says Expected ':' after property name in JSON at position 4 while pointing at what appears to be a blank. If you move text between applications a lot, run it through a cleaner that strips this class of character before it reaches a parser.
JSON hiding inside WordPress
WordPress rarely stores JSON as JSON. It stores it inside something else, and the something else is usually what broke. Block markup is the clearest case: every block’s settings are a JSON object living inside an HTML comment, between the block name and the end of the comment.
<!-- wp:image {"id":412,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large">...</figure>
<!-- /wp:image -->
The parser in wp-includes/class-wp-block-parser.php reads that object with a plain json_decode( $matches['attrs'][0], true ) and does not check the result. If the JSON is malformed, json_decode() returns null, the attributes become nothing, and every setting the block had is gone. No warning, no log entry, no error. The image reverts to defaults, or the editor tells you the block contains unexpected or invalid content. Copying that object into the box above tells you in a second what the editor never will.
The second hiding place is post meta and options, where the enemy is slashes rather than syntax. update_metadata() in wp-includes/meta.php runs wp_unslash( $meta_value ) on the way in, because it expects data that arrived from a request and was slashed. Store a JSON string with update_post_meta() without wrapping it in wp_slash() and every backslash is eaten. One omission, two failures: "u00fc" becomes "u00fc", which is still valid JSON and now says something false, while "C:logs" becomes "C:logs", which is an invalid escape and will not parse. The first kind is worse, because nothing ever errors.
The third is the one people actually search for: the block editor saying the response is not a valid JSON response. That message is the browser reporting that JSON.parse failed at position 0, which almost always means something printed before WordPress got to speak.
wp_debug_mode() in wp-includes/load.php does try to protect you. After configuring error reporting it switches display_errors off for XML-RPC requests, installs, AJAX and JSON requests. Core’s own comment on that block is worth reading: the REST_REQUEST check is called optimistic, because the constant is most likely not defined at the point the function runs. What actually saves the block editor is wp_is_json_request(), which sniffs the Accept and Content-Type headers, and the editor’s fetch layer does send Accept: application/json.
That covers PHP notices. It does nothing about a plugin that echoes, a var_dump() left in a hook, or a blank line after a closing ?> at the end of a file, because none of those are errors. They are output, and output goes out. Read the raw bytes rather than the pretty view: in devtools, open the failing wp-json request and use the Response tab, not Preview, which renders it and hides the problem. From a shell the first 300 bytes are usually the whole diagnosis.
curl -s -H 'Accept: application/json'
https://example.com/wp-json/wp/v2/types | head -c 300
# a healthy response starts with {
# a broken one starts with a warning, a notice, or <br />
If something sits in front of the brace you usually have the culprit’s file and line in the text itself. Start the bisect at the theme, since functions.php is where stray output most often comes from, then work through plugins. Set WP_DEBUG_DISPLAY to false and WP_DEBUG_LOG to true so notices go to a file instead of into your responses; both constants belong in wp-config.php, above the line that requires wp-settings.php. And if the response is empty rather than noisy, this is not a JSON problem at all but a fatal error that stopped PHP before it could write anything.
Where the tool stops
Two things are deliberately not repaired. A second document after the first is reported and left alone, because {"a":1}{"b":2} could be a mistake or could be newline delimited JSON, and dropping either half risks throwing away the half you wanted. Nesting past 120 levels is reported rather than fixed; for comparison json_decode() gives up at depth 512 by default and returns error code 1 instead of 4, one of the few times PHP is specific.
Because this is a public page rather than a build step, the guards are hard: 500,000 characters of input, 200 problems tracked, 60 problem rows drawn, 150 highlight marks, 1,200 tree rows, and a line diff that gives up on a changed stretch longer than 400 lines rather than freezing the tab on an enormous comparison table. Beyond those sizes you want a local parser and a script, not a text box. One cosmetic limit: the highlight layer is a mirror under the textarea sharing its font metrics, and on a very long unbreakable token the two can drift by about a pixel. The problem list stays accurate, only the coloured run moves.
The tree view is for the other half of the job, which is understanding a document rather than fixing it. Every row carries its JSON Pointer (/items/0/meta/title), and the pointer field with its copy button sends that path straight into a jq expression or a test assertion.
None of this makes the parsers better. V8 will keep naming the character after the mistake and PHP will keep saying two words, because both are describing their own state accurately and neither was built to guess. The gap is not in the grammar, which is small, complete and public. It is in the reporting, and the fix for bad reporting is a reader that carries on after the first fault rather than one that stops.
In WordPress especially, the JSON is rarely what broke. What went wrong is the wrapper: three invisible bytes at the front of a file, a slash removed by a function doing its documented job, a quote replaced by a nicer looking quote on the way through a word processor, or a warning printed one hook before the response was supposed to begin. When a validator says the document is clean and the parser still refuses it, the answer is in the bytes on either side of the JSON rather than inside it.
So read the error as a coordinate, not an accusation. It marks the first place the machine could no longer pretend everything was fine, and the thing you typed wrong is behind it, usually within a token or two. Most JSON debugging is then a short scan backwards from a known point rather than a stare at a file that looks perfectly correct.