Skip to content
datahacks.dev
JSON

Fixing JSON That Is Really a JavaScript Object Literal

Text copied from console.log, DevTools or a config file is often a JavaScript object, not JSON. What differs, what is lost, and how to convert it safely.

7 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the JSON logo.

You copy an object out of the browser's developer tools, a Node terminal, a JavaScript config file or a code snippet in a pull request, paste it into a JSON file, and the parser stops at the first key. The thing you copied is a JavaScript object literal. It has braces, colons and commas, so it looks like JSON, and it follows different rules. This article lists exactly where the two differ, how to convert a literal with a parser and not with eval, what cannot be recovered from console output, and how to stop producing this text in the first place.

Where an object literal departs from JSON

JSON was derived from JavaScript's literal syntax and then frozen as a stricter subset. A literal can use everything below and JSON can use none of it:

JavaScript literalJSON requires
Unquoted keys: { name: "Ada" }Keys in double quotes
Single-quoted strings: 'Ada'Double-quoted strings
Trailing commas: [1, 2,]No trailing comma
Comments: // note, /* note */No comments
undefined as a valueNot available; omit the key or use null
NaN, Infinity, -InfinityNot available
Hex, octal, leading + or .5 numbersDecimal numbers with a leading digit
Functions, Date, Map, Set, RegExp, BigIntOnly objects, arrays, strings, numbers, booleans and null
Template strings and multi-line stringsSingle-line strings with escapes

The first three are syntax and a parser can read them. The last two rows are about values, and that is where the real trouble begins, because a value that JSON cannot express has to be lost or converted. The earlier article on the single-quote problem, JSON parse error with single quotes instead of double quotes, covers the quoting row in more detail.

What console output actually is

The most common source is the terminal. console.log(obj) in Node does not print JSON; it prints the output of util.inspect, aimed at a human. On a small object that I built with a date, a BigInt, an undefined value, a function, a Map and a Set, Node 24 printed:

Text
{
  name: 'Ada',
  tags: [ 'a', 'b' ],
  deep: { l1: { l2: [Object] } },
  when: 1970-01-01T00:00:00.000Z,
  id: 12n,
  none: undefined,
  f: [Function: f],
  m: Map(1) { 'k' => 1 },
  s: Set(1) { 1 }
}

Look at what has happened to the data. deep stops at [Object], because console.log prints to a depth of 2 by default and the contents below are not shown. The date has no quotes, so it is not even a string. The BigInt carries an n suffix. The function and the Map and Set are in notations that exist only in util.inspect. Copying this and fixing the quotes cannot produce the original data, because part of the data was never printed.

The right way to get JSON from a running program is to ask for it: JSON.stringify(obj, null, 2). On the same object, with a replacer that turns BigInt into a string, it printed the nested l3 value in full, the date as "1970-01-01T00:00:00.000Z" and the BigInt as "12". It also dropped none and the function, and turned the Map and Set into {}, which is the fate of anything JSON.stringify does not understand. If you must use console.log, util.inspect(obj, { depth: null }) removes the [Object] cutoff, and it still prints the other non-JSON values in their own notation.

Converting the syntax with JSON5

For text that is a literal in the syntactic sense, with unquoted keys, single quotes, comments and trailing commas, the right tool is a parser for JSON5, a superset of JSON that adopts those parts of JavaScript's syntax. In Python:

literal_to_json.pyPython
import json
import json5

src = """{
  // copied from the dashboard config
  name: 'Ada',
  tags: ['a', 'b',],
  limit: 0x1F,
  ratio: .5,
  big: +1,
}"""
print(json.dumps(json5.loads(src), indent=2))

It printed a valid document with "limit": 31, "ratio": 0.5 and "big": 1: the hex literal and the leading-dot and plus-sign numbers were all converted to ordinary decimal numbers, and the comment and trailing commas were dropped. The reverse is also useful: json5.dumps writes unquoted keys when they are valid identifiers, and it quotes "a b". In JavaScript the json5 package has the same parse and stringify.

JSON5 also accepts Infinity and NaN. In Python, json5.loads("{x: Infinity}") returned inf, and json.dumps of that wrote the bare word Infinity, which is not valid JSON; allow_nan=False made it raise ValueError: Out of range float values are not JSON compliant. Decide what a non-finite number should become (null, a string, an error) before writing the final file. The same issue is covered in NaN and Infinity in JSON.

What the parser refuses

The parser stops, correctly, at the things that are not values. On the console output above, json5.loads("{ name: 'Ada', deep: { l1: [Object] } }") raised Unexpected "O" at column 47. A bare ISO date, when: 1970-01-01T00:00:00.000Z, stopped at column 27 on the 0, since a number cannot contain hyphens. { a: undefined } stopped at the u. These are not bugs to work around. Each one marks data that the paste does not contain.

You then have three options. Get the data again from the source with a proper serializer, which is nearly always the answer. Edit the text by hand to quote the dates and fill in the truncated parts, if you know what they are. Or decide the loss is acceptable and replace the markers with null. The third option is rare, and when you take it, do it deliberately, in a script that you keep, and not by editing the file until the parser stops complaining.

Why not eval

The shortcut that appears in many answers is eval("(" + text + ")") or new Function("return " + text)(). It does read every one of the syntax cases above, and it also executes the text. If the text contains process.exit(), a call that reads files or a network request, the code runs with your permissions. Even for text you wrote yourself, a pasted snippet may carry something you did not see. A configuration loader that evaluates its input is a code execution hole waiting for the first untrusted file. A parser reads data and nothing else. JSON5.parse does what eval did for these cases without that risk. In Node there is also vm.runInNewContext, which is a weaker tool than it looks, because the documentation states that the vm module is not a security mechanism. Use a parser.

Producing JSON in the first place

Most of the pain disappears if the text never has to be converted:

  • Log JSON, not objects. In services, use a logger that writes one JSON object per line (pino in Node, for example). Then the logs can be parsed by any tool, as in using jq to read logs written as JSON Lines.

  • **Use JSON.stringify for copy and paste.** In a browser console, copy(JSON.stringify(obj, null, 2)) puts valid JSON on the clipboard; DevTools' copy() helper is available in Chrome and Firefox consoles.

  • Name the format. A configuration file that is a literal should be called .js, .json5 or .jsonc, not .json. See JSON versus JSON5 versus JSONC and when to use each.

  • Validate on commit. A CI step that parses every .json file in the repository would have caught each of these files before it reached a deploy. For a quick check by hand, the JSON formatter and the JSON to TypeScript types tool both reject text that is not strict JSON and tell you where.

When the data cannot be recovered

A conversion is the correct move when the text is a faithful literal. It is a mistake in a few cases, and the symptom is a result that looks right.

Console output with depth cutoffs. If any [Object] or [Array] appears, data is missing. Converting the rest produces a document that is shorter than the original, and the only sign is a key with a placeholder or nothing at all. Re-run the program with depth: null, or serialize it.

Dates, Maps and Sets. A parsed date string is a string, not a date, and a Map printed as Map(1) { 'k' => 1 } has no JSON form. Decide on a representation (ISO strings, arrays of pairs, plain objects) and write it from the source program. The behavior of JSON.stringify on these values is covered in dealing with dates and BigInt in JSON.stringify.

Large integers. An id: 12345678901234567890 in a literal has already lost precision when JavaScript parsed it. If the text you have is a printout of an evaluated number, the digits you see are the rounded ones.

Executable configuration. A file with functions, require calls or computed keys is a program, not data. Convert the data it produces, not the file.

The grammar differences come from the ECMAScript specification on one side and RFC 8259 on the other, and the JSON5 format is specified at json5.org.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.