DEV Community

OnePassApps
OnePassApps

Posted on

Format JSON in the Browser: The Numbers That Change When You Reformat

I work on OnePassApps, so this is our own Data Studio and every number below is a measurement of our build in the live tab, not a description of how a JSON formatter works in general.

The Data Studio has eleven tools. Ten of them have had their own write-up on this site already - CSV to JSON, JSON to CSV, YAML, the validator, the viewer, sort and filter, columns, duplicates, merge and split. The eleventh is the Formatter, JSON tab, and it is the one nobody has written down. It is also the one worth writing down, because it does more than add whitespace.

The panel, exactly as it reads

The tab sits in the same three-step layout as the rest of the studio - "01 Source", "02 Options", "03 Result" - and the options block is two buttons:

  • "2 spaces"
  • "4 spaces"

with one line under them that tells you the most important thing about the tool:

"Keys keep their order. Changing the indent clears the result."

The result stage says "Format, size and line count appear here after the tool runs.", and the run button is labelled "Format JSON".

The sample

I pasted one deliberately awkward JSON object into the Source step - the same text every time:

{"zulu":1,"alpha":2,"unicode":"café ✓","bigint":12345678901234567890,"exp":1e999,"negzero":-0.0,"dup":1,"dup":2,"nested":{"b":[1,2,{"c":true}],"a":{}},"empty":[],"escaped":"line1\nline2\ttab \"q\" back\\slash"}
Enter fullscreen mode Exit fullscreen mode

It has an out-of-order key, a Unicode escape, a twenty-digit integer, an exponent that overflows, a negative zero, a duplicated key, an empty object, an empty array and a string with every awkward escape in it.

The result panel said:

  • "Ready"
  • "JSON is valid and formatted."
  • Size: "300 B"
  • Lines: "21"
  • "Formatted in 92 ms"

and the output stage read "GENERATED OUTPUT", "Generated JSON", "21 lines · 300 B · from Pasted JSON", with a "Download JSON" button underneath.

That is the whole result, and it is where the interesting part starts.

What the round trip keeps

Key order is kept. "zulu" comes before "alpha" in the output, exactly as it came in. That is what the options line promises, and it holds.

String escapes inside values are kept. The escaped value came out with its \n, \t, \" and \\ still escaped, not decoded into raw control characters.

Empty containers stay empty. {} and [] both survive as-is.

What the round trip changes

The Unicode escape is decoded. I wrote caf\u00e9 \u2713 in the source. The output has café ✓ - the literal characters. This is the one change most people will not notice and most people will not mind.

Three numbers change, and this is the part to care about.

Source Formatted
12345678901234567890 12345678901234567000
1e999 null
-0.0 0

The twenty-digit integer lost its last two digits. 1e999 - a number too large for a double - came back as null. And -0.0 came back as 0.

None of this is a bug in the formatter. It is what happens when JSON text is parsed into JavaScript numbers and then written back out: everything becomes an IEEE-754 double, and a double cannot hold a twenty-digit integer exactly or represent an overflowing exponent at all.

Here is the boundary, on a number nine digits shorter. Node's JSON.parse - the same implementation the panel's output matched byte for byte - on:

{"n": 9007199254740993}
Enter fullscreen mode Exit fullscreen mode

9007199254740993 is 2^53 + 1, the smallest integer a double cannot represent. Node returns 9007199254740992. Every integer at or above that point is a candidate for silent rounding - and "silent" is the operative word, because the formatted output is valid JSON and nothing in the panel says a number was touched.

The duplicate key collapses. "dup":1,"dup":2 came out as a single "dup": 2. The last one wins, as JSON requires, and the first one is simply gone.

The bytes, checked outside the browser

I clicked "Download JSON" and read the file off disk. It is called data.json, it is 300 bytes, it is 21 lines with no trailing newline, and its SHA-256 is 730f3f43d18df202b0cad7b597a46da0edf3d56695bfa4242aa6e366fe91baa5.

The panel's "300 B" is bytes, not characters. The same text is 297 characters - the difference is é and ✓ taking two and three bytes each. A size readout that counted characters would have said 297.

Then I checked the file against an independent implementation. Running the same input through Node's own JSON.stringify(JSON.parse(input), null, 2) produced byte-identical output - the same 300 bytes, the same SHA-256, 730f3f43d18df202b0cad7b597a46da0edf3d56695bfa4242aa6e366fe91baa5. The four-space run matched JSON.stringify(JSON.parse(input), null, 4) the same way: 366 bytes reported, 366 bytes produced.

So the mechanism is not a mystery. It is a parse into JavaScript numbers and a stringify back out. Our panel is the friendly face of JSON.parse plus JSON.stringify with an indent of your choice.

And Python tells you where the bytes go

Python's json.dumps on the same input produces 307 bytes, seven more than the tool. Both differences are the numbers, and both are explained by Python and JavaScript disagreeing about what a JSON number is:

  • Python writes Infinity where the tool writes null - "Infinity" is eight characters, "null" is four, so +4 bytes.
  • Python writes -0.0 where the tool writes 0 - "-0.0" is four characters, "0" is one, so +3 bytes.

Four plus three is seven, which is exactly the gap. The twenty-digit integer is 20 characters either way, so it contributes nothing to the difference in size - it just quietly carries different digits in each.

Python is also right and the tool is wrong on the big integer: json.loads keeps 12345678901234567890 exactly, because Python integers have no size limit. The tool cannot, because the browser's number type does.

When the JSON is broken

I replaced the sample with {"broken": [1,2,}. The panel refused to run it and said:

"Could not use this data"

"Line 1, column 17: Unexpected token '}', "{"broken": [1,2,}" is not valid JSON"

"1: {"broken": [1,2,}"

with "Nothing to show yet" and "Fix the problem shown above the run action, then run the tool again."

The message is V8's own JSON.parse error text. Node's JSON.parse on the identical string throws exactly Unexpected token '}', "{"broken": [1,2,}" is not valid JSON, and the reported column is 17 - the offending } is the seventeenth character of the line, counting from one. The panel adds the line number and echoes the offending line.

The limits, and where the data goes

The "Input limits and privacy" disclosure on this tab reads:

"Pasted text supports up to 10,000 lines. Pasted CSV supports up to 8 MiB and 10,000 data rows; CSV files support up to 20 MiB, 100,000 rows, and 2 million cells. JSON supports up to 2 MiB and YAML up to 1 MiB. Your data stays on this device."

I also turned the browser's network log on for the run. Across the whole format operation the page made one request: a GET to api.onepassapps.com/api/config/feature-flags. Nothing was posted, no request carried a body, and no request URL carried any part of the pasted text. The formatting itself happened in the tab.

What I did not test

  • One browser, one build today, signed out, one sample.
  • I pasted the JSON rather than loading it from a file, so I did not exercise the "Browse files" path even though the panel offers it.
  • I clicked "Format JSON", "4 spaces" and "Download JSON". I did not click "Clear" or "Format again", so I make no claim about what they do beyond the panel's own wording.
  • Only one malformed input, so I make no claim about how every kind of broken JSON is reported.
  • The downloaded file is what I read off disk; the on-screen preview and the file were the same 300 bytes, and I compared both to an independent implementation.

Where to try it

The formatter lives in the Data Studio, next to the other ten tools, and the app guides describe what each studio does and will not do: Data Studio and the guides. It runs in your browser tab, needs no account, and takes no credits.

If the JSON you care about holds a large integer, check it before you reformat it - and if it does not, the output is a clean, key-ordered, byte-for-byte predictable JSON.stringify.

Top comments (0)