[{"data":1,"prerenderedAt":35},["ShallowReactive",2],{"$f5cD1orzbkcBrIFlShavHM8jwMVXo03tlogLhCjyh-xQ":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":15,"html":16,"toc":17,"minutes":34},"writing-a-json-parser-for-exact-errors","Why My JSON Formatter Has Its Own Parser Instead of JSON.parse","Big numbers that JSON.parse rounds, errors with a line and column, and duplicate keys: how ToolOnTap's JSON formatter parses JSON itself in about 200 lines.","2026-10-15",[9,10,11,12],"json","parsing","javascript","typescript",[14],"json-formatter",false,"\u003Cp>You paste an API response into an online formatter to read it, and copy an order ID out of the pretty version. The ID doesn’t exist. The formatter quietly changed \u003Ccode>12345678901234567890\u003C\u002Fcode> into \u003Ccode>12345678901234567000\u003C\u002Fcode>, because it read your JSON with \u003Ccode>JSON.parse\u003C\u002Fcode>. Or the formatter tells you \u003Ccode>Expected ',' or '}' after property value in JSON at position 52\u003C\u002Fcode>, and you count characters in a 3,000-line file.\u003C\u002Fp>\n\u003Cp>A JSON formatter seems like the one tool where the browser does all the work. When I built ToolOnTap’s \u003Ca href=\"\u002Fjson-formatter\">JSON formatter\u003C\u002Fa>, I ended up writing my own parser instead, for three reasons.\u003C\u002Fp>\n\u003Ch2 id=\"problem-one-json-parse-changes-numbers\">Problem one: JSON.parse changes numbers\u003C\u002Fh2>\n\u003Cp>JavaScript has one number type, a 64-bit float. It holds every integer exactly up to 2^53 − 1, which is 9,007,199,254,740,991, a 16-digit number. Beyond that, \u003Ccode>JSON.parse\u003C\u002Fcode> rounds to the nearest number it can store. The JSON grammar has no such limit, since a number is just digits, though RFC 8259 warns that software may not handle numbers beyond that range the same way.\u003C\u002Fp>\n\u003Cp>This is what a format done with \u003Ccode>JSON.parse\u003C\u002Fcode> and \u003Ccode>JSON.stringify\u003C\u002Fcode> does to some ordinary-looking values (run in Node 20):\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>In your JSON\u003C\u002Fth>\n\u003Cth>After JSON.parse and JSON.stringify\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>12345678901234567890\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>12345678901234567000\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>9007199254740993\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>9007199254740992\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>1.50\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>1.5\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>1e400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>null\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>-0\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>0\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>&quot;café&quot;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>&quot;café&quot;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>Some of those are harmless. A 19-digit ID that changes, or a price that loses its trailing zero before a strict comparison, is not. And a formatter that changes your data is no longer a formatter.\u003C\u002Fp>\n\u003Cp>So my parser never converts a value. A number, string, \u003Ccode>true\u003C\u002Fcode>, \u003Ccode>false\u003C\u002Fcode> or \u003Ccode>null\u003C\u002Fcode> is kept as the exact text it was written as:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> type\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> JsonNode\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  |\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">kind\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'object'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">entries\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">key\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">raw\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">value\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> JsonNode\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }[] }  \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F key: decoded, raw: as written\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  |\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">kind\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'array'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">items\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> JsonNode\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">[] }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  |\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">kind\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> 'value'\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">raw\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }                                                 \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F string, number, true, false, null\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The printer writes \u003Ccode>raw\u003C\u002Fcode> back out. For ordinary JSON, the output is identical to \u003Ccode>JSON.stringify(value, null, indent)\u003C\u002Fcode>, which a test checks with 2 spaces, 4 spaces and tabs. For JSON that \u003Ccode>JSON.parse\u003C\u002Fcode> would change, the output keeps every digit and every escape.\u003C\u002Fp>\n\u003Cp>Keeping the text doesn’t mean accepting anything. Numbers must match the grammar in RFC 8259, the JSON standard, and the parser uses a sticky regular expression (the \u003Ccode>y\u003C\u002Fcode> flag) so it matches exactly at the current position without slicing the string:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> NUMBER\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> \u002F\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">-\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">(?:0\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">|\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">[1-9]\\d\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">)(?:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#1a7f37;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">\\d\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">)\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">(?:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">[eE][+-]\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">\\d\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#DBEDFF\">)\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">?\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">\u002F\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">y\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>That rules out \u003Ccode>01\u003C\u002Fcode>, \u003Ccode>.5\u003C\u002Fcode>, \u003Ccode>1.\u003C\u002Fcode> and \u003Ccode>+1\u003C\u002Fcode>, which are all valid JavaScript but not valid JSON. A test runs a list of edge cases through both my parser and \u003Ccode>JSON.parse\u003C\u002Fcode> and checks that they agree on what’s valid.\u003C\u002Fp>\n\u003Ch2 id=\"problem-two-errors-you-can-act-on\">Problem two: errors you can act on\u003C\u002Fh2>\n\u003Cp>\u003Ccode>JSON.parse\u003C\u002Fcode> only tells you whether the text is valid, and its error messages depend on the browser. In Node 20, which uses the same engine as Chrome, a missing comma on line 4 gives \u003Ccode>Expected ',' or '}' after property value in JSON at position 52\u003C\u002Fcode>. Firefox and Safari word the same error differently.\u003C\u002Fp>\n\u003Cp>My parser knows where it is, so every error has a line and a column, and a message written for the person, not the parser. The position is worked out only when something goes wrong, by counting line breaks up to the error:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> positionAt\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">text\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">offset\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> JsonPosition\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  let\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  let\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> lineStart \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  for\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">let\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> i \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; i \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> offset \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> i \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> text.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">length\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; i\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (text.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">charCodeAt\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(i) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">===\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 10\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) { line\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">; lineStart \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> i \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">  }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { offset, line, column: offset \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> lineStart \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The messages target the mistakes people actually make. Most broken JSON started as JavaScript, Python or a hand-edited config file:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Input\u003C\u002Fth>\n\u003Cth>Message\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: 1,}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Trailing comma: remove the comma before }.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{'a': 1}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>JSON strings need double quotes (&quot;), not single quotes.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{a: 1}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Property names need double quotes, like “name”.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: 1 \u002F\u002F note\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>JSON doesn’t allow comments. Remove the comment.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: undefined}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>undefined isn’t a JSON value. Use null, or a number or string instead.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: True}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>JSON is case-sensitive: write true.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: None}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>JSON is case-sensitive: write null.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>[1 2]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Missing comma between two items.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>{&quot;a&quot;: 1} {&quot;b&quot;: 2}\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Unexpected text after the end of the JSON. There may be a missing [ or { at the start, or more than one value.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>\u003Ccode>True\u003C\u002Fcode> and \u003Ccode>None\u003C\u002Fcode> are there because people paste Python output. “Missing comma” needed its own check: when the parser finishes a value and sees a \u003Ccode>&quot;\u003C\u002Fcode> where a comma or \u003Ccode>}\u003C\u002Fcode> should be, the missing comma is almost always the cause, so it says that instead of “unexpected character”.\u003C\u002Fp>\n\u003Cp>The tool also doesn’t try to repair JSON. Guessing where a quote or a bracket should go could change what the data means. It points to the exact spot and lets you fix it.\u003C\u002Fp>\n\u003Ch2 id=\"problem-three-duplicate-keys\">Problem three: duplicate keys\u003C\u002Fh2>\n\u003Cp>\u003Ccode>{&quot;a&quot;: 1, &quot;a&quot;: 2}\u003C\u002Fcode> is valid JSON. RFC 8259 only says that names within an object SHOULD be unique, and that software behaves unpredictably when they aren’t. \u003Ccode>JSON.parse\u003C\u002Fcode> keeps the last value without a word, so in a formatter built on it, the first \u003Ccode>&quot;a&quot;\u003C\u002Fcode> simply disappears.\u003C\u002Fp>\n\u003Cp>My parser keeps both entries and adds a warning with the position of the second one:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> key\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> JSON\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">parse\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(raw) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">as\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> string\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">  \u002F\u002F the token is already validated\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (seen.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">has\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(key)) warnings.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">push\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">({ \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">...\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">positionAt\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(text, keyAt), message: \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">`Duplicate key ${\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">raw\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\">}. Most programs keep only the last one.`\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> })\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">seen.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(key)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This is the one place it uses \u003Ccode>JSON.parse\u003C\u002Fcode>, on a single string token that has already been validated, to decode its escapes. Comparing decoded keys means \u003Ccode>&quot;a&quot;\u003C\u002Fcode> and \u003Ccode>&quot;a&quot;\u003C\u002Fcode> count as the same key, which is how any program reading the JSON will see them. The formatted output still shows both entries, as written, so you can decide which one to keep.\u003C\u002Fp>\n\u003Ch2 id=\"staying-safe-on-bad-input\">Staying safe on bad input\u003C\u002Fh2>\n\u003Cp>A parser for text people paste has to survive anything. Three limits keep it predictable:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Depth.\u003C\u002Fstrong> The parser is recursive, so \u003Ccode>[[[[…\u003C\u002Fcode> nested a hundred thousand times could exhaust the call stack. It stops at 1,000 levels with a clear message instead.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Size.\u003C\u002Fstrong> The text box and file picker accept up to about 20 MB. Beyond that the browser’s text box itself becomes the slow part.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Typing.\u003C\u002Fstrong> The parser runs 150 ms after you stop typing, or 400 ms for text over 200,000 characters, so a large paste doesn’t re-parse on every keystroke.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>All of it is about 200 lines of TypeScript with no dependencies, which also means it parses the same way in every browser.\u003C\u002Fp>\n\u003Ch2 id=\"what-it-gives-you\">What it gives you\u003C\u002Fh2>\n\u003Cp>Paste JSON or open a \u003Ccode>.json\u003C\u002Fcode> file in the \u003Ca href=\"\u002Fjson-formatter\">JSON formatter\u003C\u002Fa>. Format it with 2 spaces, 4 spaces or tabs, minify it, or sort keys at every level so two files can be compared in a diff. Your values stay exactly as you wrote them. More tools for this kind of work are on the \u003Ca href=\"\u002Fdev-tools\">developer tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[18,22,25,28,31],{"id":19,"text":20,"level":21},"problem-one-json-parse-changes-numbers","Problem one: JSON.parse changes numbers",2,{"id":23,"text":24,"level":21},"problem-two-errors-you-can-act-on","Problem two: errors you can act on",{"id":26,"text":27,"level":21},"problem-three-duplicate-keys","Problem three: duplicate keys",{"id":29,"text":30,"level":21},"staying-safe-on-bad-input","Staying safe on bad input",{"id":32,"text":33,"level":21},"what-it-gives-you","What it gives you",5,1792325800858]