Developer Tools

JSON Validator and Repair: What the Parser Actually Saw

Parsers name the character after the mistake, and PHP names nothing at all. The whole JSON grammar, the invisible bytes that break a file that looks perfect, the places WordPress hides JSON, and a validator that reports every fault in one pass.

JSON Validator and Repair: What the Parser Actually Saw

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.

empty
What is wrong

    Repairs

    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.

      WunderPaint
      The Dynamic Design and Automation Studio
      WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

      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”.

      Table of the complete JSON grammar: six structural characters, four allowed whitespace bytes, three lowercase literal words, double quoted strings only, a fixed number shape, and no comments, each row paired with what the parser rejects.

      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.

      Flow diagram of a failing WordPress REST response: the editor posts to wp-json, a plugin prints stray output first, WordPress appends valid JSON after it, JSON.parse fails at position 0, and the editor reports that the response is not a valid JSON response.

      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.

      JSON Validator and Repair: What the Parser Actually Saw

      Table of Contents

      Learn it by building something

      Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

      One mail a week, and then it ends.
      Unsubscribe in one click.

      Security & Privacy

      Black out every piece of text in a screenshot before you share it

      A tool that guesses which text is sensitive will miss the one that mattered. This one covers every text field it finds and lets you click back what can stay. It also refuses to default to a blur, because blurring and pixelation can be undone, and there is published work showing exactly how.

      Troubleshooting

      HTTP Error When Uploading Images to WordPress: The Real Causes

      The red bar reading HTTP error is not a verdict on your image. It is the uploader reporting that the POST to async-upload.php came back unusable, and six unrelated failures produce exactly that. Here is how to tell them apart in about ten seconds.

      SEO & Structured Data

      WordPress Alt Text: Where It Lives and Why Your Edits Do Not Show

      Alt text in WordPress is one postmeta row, copied into the post markup the moment you insert an image. That single fact explains why editing the library changes nothing on pages you already published, why an audit has two halves, and what to fix first.

      WordPress Development

      wp-config.php Explained: The Constants That Earn Their Place

      wp-config.php is the first file WordPress reads that is specific to your install, and almost every fix you will ever be handed ends with "add this line to wp-config.php". Here is what the file actually does, which constants earn their place, and why the placement rule matters.

      Security & Privacy

      Screenshots Worth Publishing: Capture, Frame, Annotate

      An enormous amount of what people need to see is a picture of a screen, and most of those pictures are worse than they need to be for reasons that take seconds to fix.

      WordPress Development

      One Design, Two Hundred Names: Designing From a Spreadsheet

      Somebody needs two hundred name badges by Thursday. The spreadsheet with all those names already exists, and typing them a second time is pure waste.

      Download the free WunderPaint Plugin for WordPress

      The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

      The Image Editor & Design Studio

      Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

      Free

      Chaos Art

      Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

      Pro

      Particle Strokes

      Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

      Pro

      City Diorama

      Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

      Free

      Papercut Art

      Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.