What Is a JSON File? Structure, Errors and Practice Explained (2026)

What Is a JSON File? Structure, Errors and Practice Explained (2026)

Sooner or later one shows up: a file called config.json, package.json or export.json. Double-clicking it either does nothing at all, opens a browser showing a collapsible tree, or drops you into an editor with one endless line full of curly braces.

A JSON file is a plain text file that stores structured data in a fixed format. JSON stands for JavaScript Object Notation. It contains no program logic, no formatting and no hidden metadata — only values, names and the nesting between them. Any program that can read text can open a JSON file, and every mainstream programming language ships with a parser for it.

This article explains the whole thing from scratch: what is actually inside, which data types exist, why the parser gives up over a single extra comma, how JSON differs from YAML, TOML and the variants JSONC, JSON5 and NDJSON, what happens with large files — and the two places where JSON turns into a security problem. Every number and error message in this article comes from measurements on our own server, not from documentation.

The short answer, in six sentences

  1. A JSON file is plain text — the same kind of data as in a .txt file.
  2. It stores named values, nested inside each other to any depth.
  3. There are exactly six data types: string, number, boolean, null, array and object.
  4. The syntax is extremely strict — one extra comma makes the entire file unreadable.
  5. Every language can read it; JSON is not a JavaScript feature, even though the name suggests otherwise.
  6. You can open it with any text editor, more comfortably with one that colours syntax.

The most important sentence for anyone who just wants to look at a JSON file: you need no special software. Notepad, TextEdit, nano, VS Code — all of them work. The file is readable as soon as it is wrapped and indented.

How a JSON file is structured

The following example contains everything JSON is capable of:

{
  "name": "getmind",
  "version": 2,
  "active": true,
  "description": null,
  "tags": ["blog", "tech", "english"],
  "author": {
    "firstName": "Fabian",
    "roles": ["engineering", "editorial"]
  }
}

The building blocks:

  • Object — written in curly braces {}, containing pairs of name and value. The name is always in double quotes.
  • Array — written in square brackets [], containing an ordered list of values. The values may have different types, even though that is rarely a good idea.
  • String — double quotes, always. Single quotes are forbidden.
  • Number — no quotes, a dot as decimal separator, optionally with an exponent (1.5e3).
  • Booleantrue or false, lowercase.
  • Nothingnull, lowercase.

The outermost element is usually an object or an array. But a single value is legal too: 42, "hello" or true are valid JSON documents on their own. That surprises many people and is the reason some APIs are allowed to return a bare number.

Six abstract geometric objects representing the six JSON data types

What JSON deliberately cannot do

This list explains most moments of frustration:

  • No comments. There is no official syntax for them. That was an explicit decision by its creator Douglas Crockford — comments used to be abused to smuggle in parser directives.
  • No dates. JSON has no date type. Timestamps are always strings, conventionally in the format 2026-09-16T07:00:00Z (ISO 8601).
  • No variables, no references. A JSON file cannot refer to itself.
  • No trailing comma. No comma is allowed after the final entry — the single most common mistake.
  • No encoding other than UTF-8 in practice. The standard technically permits more, every relevant parser expects UTF-8.

How to open a JSON file

Depending on what you intend to do:

Just looking: Drag the file into a browser window. Firefox and Chrome have a built-in JSON viewer with a collapsible tree. That is the fastest route and requires no installation.

Editing: VS Code, Notepad++, Sublime Text or any other code editor. These colour the syntax and flag errors in red as you type — which saves the hunt later.

Formatting (the infamous single long line): Many files are stored without line breaks because it saves bytes. You can indent them in any editor or on the command line:

# with jq (the standard tool on servers)
jq . file.json > readable.json

# with Python, no extra install needed
python3 -m json.tool file.json

# with Node.js
node -e "console.log(JSON.stringify(require('./file.json'), null, 2))"

Searching on a server: jq is the tool of choice. jq '.author.firstName' file.json extracts a single value without opening the file.

What you should not do: open JSON files in Word or any word processor. Word automatically replaces straight quotes with typographic (“smart”) ones — and makes the file unusable in the process. Exactly how unusable is the subject of the next section.

The most common syntax errors — with the real parser messages

JSON forgives nothing. We pushed eleven typical mistakes through the Node.js 22 parser and wrote down the messages instead of paraphrasing them:

MistakeInputMessage from JSON.parse
Trailing comma{"a":1,}Expected double-quoted property name in JSON at position 7
Single quotes{'a':1}Expected property name or '}' in JSON at position 1
Unquoted key{a:1}Expected property name or '}' in JSON at position 1
NaN as a value{"a":NaN}Unexpected token 'N', "{"a":NaN}" is not valid JSON
Comment at the end{"a":1} // hiUnexpected non-whitespace character after JSON at position 8
Infinity{"a":Infinity}Unexpected token 'I', ... is not valid JSON
Leading zero{"a":01}Unexpected number in JSON at position 6
Hexadecimal number{"a":0x1F}Expected ',' or '}' after property value in JSON at position 6
One brace too many{"a":1}}Unexpected non-whitespace character after JSON at position 7
BOM at the start\uFEFF{"a":1}Unexpected token '', "{"a":1}" is not valid JSON
Typographic quotes{"a":1} with ” and ”Expected property name or '}' in JSON at position 1

Two things stand out.

First: the reported position often does not point at the cause. For the trailing comma the parser reports position 7 — that is the closing brace, not the comma at position 6. The parser only notices there that something promised by the comma never arrived. In a 4,000-line file you therefore do not search at the reported position but immediately before it.

Second: the BOM message is misleading. The text after “Unexpected token” looks like valid JSON: {"a":1}. The offending byte is invisible — a byte order mark that some Windows editors prepend when saving. The file looks correct in every editor and is broken anyway. When a file “looks right” and the parser still complains, check this first:

file file.json            # reports "with BOM" if present
head -c 3 file.json | xxd # EF BB BF = BOM

A chain of glowing crystal links with a single one cracked and glowing red

Same file, different parser, different result

Here it gets uncomfortable: parsers do not agree. We ran the same inputs against Python 3:

InputNode.js (JSON.parse)Python (json.loads)
{"a":NaN}erroraccepted, yields nan
{"a":Infinity}erroraccepted, yields inf
{"a":1,}errorerror
{'a':1}errorerror

Python accepts NaN and Infinity — neither of which is in the JSON standard. Worse still: Python writes them out by default too. json.dumps({"a": float("nan")}) produces {"a": NaN}, a file no JavaScript parser can read. Preventing that requires asking explicitly:

json.dumps(data, allow_nan=False)   # now raises instead of writing invalid JSON

This is why a file “works on my machine” and not on the recipient’s. Valid is not an absolute state — it is a statement about one particular parser.

The number traps nobody expects

JSON has exactly one numeric type. How it is implemented is left to the language by the standard — and JavaScript uses double-precision floating point for it. That has measurable consequences:

JSON.parse('{"id": 9007199254740993}')
// yields: { id: 9007199254740992 }   ← the last digit is gone

The number was silently altered while being read. No warning, no error. Above Number.MAX_SAFE_INTEGER (9,007,199,254,740,991) every integer in JavaScript is an approximation. Python reads the same file exactly:

json.loads('{"id": 9007199254740993}')   # {'id': 9007199254740993}  — correct

The practical consequence: database IDs, snowflake IDs (Discord, Twitter) and large counters belong in JSON as strings, not as numbers. That is exactly why the Discord API returns IDs as "1528140044693405807" in quotes — not sloppiness, but the only way to get them through JavaScript undamaged.

And the second number trap, which has the same root cause:

JSON.stringify({x: 0.1 + 0.2})   // {"x":0.30000000000000004}

Monetary amounts therefore do not belong in JSON as floating point numbers either. Use an integer count of cents (1999) or a string ("19.99").

What JSON.stringify quietly swallows

Writing has the mirror-image problem: values JSON does not know disappear without an error. Measured:

JSON.stringify({
  a: undefined, b: () => 1, c: Symbol('s'),
  d: new Date(0), e: NaN, f: Infinity,
  g: new Map([[1,2]]), h: new Set([1])
})
// {"d":"1970-01-01T00:00:00.000Z","e":null,"f":null,"g":{},"h":{}}

Three different behaviours in a single call:

  • undefined, functions and symbols drop out entirely — the keys a, b and c no longer exist in the output.
  • NaN and Infinity become null — the value survives, its meaning does not.
  • Map and Set become empty objects — the contents are gone while the structure still looks intact. This is the most dangerous case, because nothing is missing that anyone would notice.
  • Date becomes an ISO string. When read back it is no longer a date but text. JSON.parse(JSON.stringify(x)) is therefore not a lossless copy.

Only two things actually throw: BigInt (Do not know how to serialize a BigInt) and objects that reference themselves (Converting circular structure to JSON).

Duplicate keys: allowed, but unpredictable

JSON.parse('{"a":1,"a":2}')   // { a: 2 }  — the last one wins

The standard does not forbid duplicate names, but it also does not say which one should apply. Node and Python both take the last one — that is not guaranteed. In security contexts this is a known entry point: if a checking system reads the first value and the target system reads the last, a validation step can be bypassed.

JSON, JSONC, JSON5, NDJSON: the variants

Because JSON is deliberately so strict, spin-offs appeared. They look similar and are not interchangeable.

JSONC — JSON with Comments. Allows // and /* */ as well as trailing commas. Microsoft uses it for tsconfig.json and the VS Code settings. That creates a situation which exists on our own server: a file with a .json extension that TypeScript reads perfectly and JSON.parse rejects.

$ node -e "JSON.parse(require('fs').readFileSync('touchline/tsconfig.json','utf8'))"
Expected double-quoted property name in JSON at position 129 (line 7 column 5)

At position 129 sits a comment explaining why a particular setting is necessary. A sensible line in a file that does not honour its own extension. Remember: .json as a file extension is a claim, not a guarantee.

JSON5 — goes further: unquoted keys, single quotes, hexadecimal numbers, multi-line strings, Infinity and NaN. Convenient for hand-written configuration, but it needs its own library. Unsuitable for data exchange.

NDJSON / JSON Lines (.ndjson, .jsonl) — one complete JSON object per line, with no enclosing array and no commas between entries:

{"type":"connection","intensity":0.8,"timestamp":"2026-05-11T23:08:34Z"}
{"type":"curiosity","intensity":0.5,"timestamp":"2026-05-12T08:11:02Z"}

This is not a curiosity but the format practically all server logs arrive in. Our Caddy web server writes every single request as one such line; the access file for getmind.io contained 34,687 lines while this article was being written. The advantage is quantified below.

JSON vs YAML vs TOML: which format when

All three store the same kind of data. The differences lie in who writes them and who reads them.

JSONYAMLTOML
Commentsnoyesyes
Indentation is meaningfulnoyesno
Writing by handtediouspleasantpleasant
Generating by machineidealriskyrare
Nestingany depthany depthflat preferred
Parser included in every languageyesnono
Ambiguitiesvirtually noneseveral knownfew

JSON is the format for exchanging data between programs. Its strictness is an advantage there: there is barely any room for interpretation, and every language ships a parser.

YAML is designed for humans to write — Kubernetes, GitHub Actions, Docker Compose. The price is ambiguity. The most famous trap is the “Norway problem”: in YAML 1.1 NO is read as the boolean false, which is why unquoted country codes flip silently. On top of that, indentation carries meaning — one space too many changes the structure without raising an error.

TOML sits in between. Unambiguous like JSON, readable like an INI file, with comments. Standard for Rust’s Cargo.toml and Python’s pyproject.toml. It gets unwieldy with deep nesting.

The rule of thumb: if a human writes the file and a program reads it, use TOML or YAML. If a program writes and a program reads, use JSON. And when you need comments in a .json, usually JSON is not the problem — the choice of format is.

Three parallel columns of glowing liquid as a comparison visual

Large JSON files: what actually happens

The common claim is that “large JSON files are slow”. That is imprecise. We measured the largest JSON file on our server — a language model tokenizer file of 32.8 MB:

readFileSync : 246 ms
JSON.parse   : 310 ms
RSS before   :  45 MB
RSS after    : 255 MB

Reading and parsing together take a little over half a second — that is not the problem. The problem is the last line: 32.8 MB of file become 255 MB of memory, a factor of 7.8. The reason is that every key, every string and every number becomes its own object with bookkeeping overhead in memory. Anyone wanting to parse a 500 MB file does not need 500 MB free, but several gigabytes.

On top of that sits a hard limit that is independent of available RAM:

require('buffer').constants.MAX_STRING_LENGTH / 1048576   // 512 (MB)

A string in Node.js can be at most around 512 MB. A JSON file larger than that cannot even be read with readFileSync(..., 'utf8') — the error arrives before the parser does, and a bigger server does not help.

Streaming instead of loading — and how big the difference really is

We generated the same data in two formats and asked the same question (how many records have score > 995). Once as one large array, once as NDJSON:

At 200,000 records (15.9 MB):

array, fully parsed  : 0.21 s, max RSS 110,360 KB
NDJSON, line by line : 0.26 s, max RSS  66,640 KB

Here the difference is small — streaming was even marginally slower. Had the measurement stopped here, the conclusion would have been “the difference is negligible”. So we multiplied the dataset by six.

At 1,200,000 records (96.7 MB):

array, fully parsed  : 0.95 s, max RSS 371,464 KB
NDJSON, line by line : 0.89 s, max RSS  74,836 KB

Now it is obvious: the array’s memory footprint grew by a factor of 3.4 (110 MB → 371 MB), the streaming one barely at all (67 MB → 75 MB, and around 45 MB of that is the Node process itself). On time there is no relevant difference.

That is the actual point, and it is often told wrong: streaming is not faster. Streaming is memory-constant. Runtime grows linearly with the data in both cases; memory does so in only one of them. A process that loads a file completely works flawlessly in development with small test data and gets killed by the OOM killer in production — on the same line of code.

The same holds on the command line. On the 32.8 MB file:

jq .version            : 0.77 s, max RSS 251,400 KB
jq --stream (first 3)  : 0.01 s, max RSS   3,968 KB

jq --stream reads the file in chunks instead of building the whole tree — a factor of 63 on memory.

A stream of glowing data droplets flowing through a narrow channel past a small reading device

The practical advantage of NDJSON

Because every line is valid on its own, questions can be answered without knowing the file. On our reward log (2,006 lines, 500 KB):

tail -1 file.jsonl | jq -c '.type'   # 0.01 s, 3,968 KB RSS
jq -s '.[-1].type' file.jsonl        # 0.02 s, 5,120 KB RSS

At this size the difference is meaningless — with a log file of several gigabytes the first variant is the only one that completes at all. And then there is the actual reason for the format: you can append to an NDJSON file. With a JSON array you would have to find the closing bracket, remove it, write, and put it back — which breaks under concurrent access. That is why logging systems write NDJSON and not arrays.

Security: two places where JSON becomes dangerous

eval instead of JSON.parse

Old tutorials occasionally suggest reading JSON with eval() since it is JavaScript syntax anyway. That is technically true and a security disaster: eval executes everything in the string. A manipulated server can run arbitrary code in the user’s browser this way. JSON.parse is a pure parser — it cannot execute anything because it does not understand code. There is no situation where eval is the right choice for JSON.

Prototype pollution

This one is subtler, and the confusion around it is widespread. JSON.parse itself is not vulnerable. We measured it:

const p = JSON.parse('{"__proto__":{"x":1}}');
Object.getOwnPropertyNames(p)   // [ '__proto__' ]  — an ordinary key
({}).x                          // undefined        — nothing happened

JSON.parse creates __proto__ as a regular own property. The danger only arises from what happens to the data afterwards — for instance a hand-rolled recursive merge of configuration objects:

function badMerge(target, source) {
  for (const k in source) {
    if (typeof source[k] === 'object' && source[k] !== null) {
      if (!target[k]) target[k] = {};
      badMerge(target[k], source[k]);
    } else target[k] = source[k];
  }
  return target;
}

badMerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

({}).isAdmin        // true
const user = {};
user.isAdmin        // true   ← a freshly created, empty object

After that single call every object in the entire process carries isAdmin: true — including all objects created later. A check like if (user.isAdmin) now says “yes” for every user.

The defence is simple once you know where the problem sits:

// 1. Filter keys while parsing
JSON.parse(text, (key, value) => key === '__proto__' ? undefined : value);

// 2. Use an object with no prototype
const safe = Object.assign(Object.create(null), JSON.parse(text));

// 3. Do not write your own merge — validate the structure
//    (Zod, Ajv with JSON Schema) — unknown keys are dropped anyway

The underlying pattern is worth remembering: the risk is not reading the data, it is adopting its structure unchecked. How this class of vulnerability fits into the bigger picture is something we wrote up in our article on IT security vulnerabilities — including an incident of our own where a break-in went unnoticed for five days.

A glass tower with a dark substance seeping upward through every floor

Validating and formatting JSON without uploading data

With many online validators the pasted data ends up on somebody else’s server. For a sample file that is irrelevant; for a database export or a configuration containing credentials it is not.

The alternatives that run locally:

jq empty file.json && echo "valid"      # parses, prints nothing, the exit code is the answer
python3 -m json.tool file.json > /dev/null && echo "valid"
node -e "JSON.parse(require('fs').readFileSync('file.json','utf8'))" && echo "valid"

jq empty is the tersest variant: it parses the file completely, outputs nothing, and on failure produces a message with a line number. In a script or a Git hook this is the right check — it can turn red.

If you just want to make something structured readable without installing a tool: our Markdown editor runs entirely in the browser and sends nothing to a server. The same principle applies to our image compressor — data that never leaves the machine cannot leak.

Frequently asked questions about JSON files

What is a JSON file in simple terms?

A JSON file is a plain text file in which data is stored in a structured way — names with associated values, nested inside one another to any depth. It contains no program logic and no formatting, only content and structure. You can open it with any text editor, and practically every programming language can read it without additional software.

How do I open a JSON file?

With any text editor, including the pre-installed one. The most comfortable option is a code editor such as VS Code, because it colours the syntax and flags errors. For simply viewing it, drag the file into a browser window — Firefox and Chrome display it as a collapsible tree. Avoid word processors like Word: they automatically replace straight quotes with typographic ones and thereby make the file invalid.

What is JSON used for?

Primarily for exchanging data between programs: almost every web API answers in JSON. Beyond that, for configuration files (package.json, composer.json), for server logs in NDJSON format, for database exports and for structured data in web pages. JSON replaced XML in most of these roles because it is more compact and easier to process.

Can you write comments in JSON?

No, the standard does not provide for comments — that was a deliberate decision. There are two usual workarounds: the JSONC variant (comments allowed, used by tsconfig.json and VS Code) or an extra field such as "_comment": "..." that the parser treats as a normal value. If you need comments regularly, TOML or YAML is usually the better-suited format.

What does the error “Unexpected token in JSON” mean?

The parser hit a character that is not permitted at that position. The most common causes are a comma after the final entry, single instead of double quotes, a key without quotes, or an invisible BOM at the start of the file. Important: the reported position often does not point at the cause but at the place where the problem became apparent — so search immediately before it.

What is the difference between JSON and NDJSON?

A JSON file contains exactly one document, typically one large array. An NDJSON file (also called JSON Lines) contains one complete, self-contained JSON object per line. The practical advantage: you can append without knowing the file, and process it line by line without loading everything into memory. In our measurement, memory use while streaming stayed nearly constant going from 200,000 to 1,200,000 records, while the array approach rose from 110 MB to 371 MB.

Why does my large number change in JSON?

Because JavaScript treats all numbers as double-precision floating point. Above 9,007,199,254,740,991 every integer is only an approximation — 9007199254740993 silently becomes 9007199254740992 on reading, with no error. Large IDs therefore belong in JSON as strings in quotes. That is exactly what Discord and Twitter do with their snowflake IDs.

Is JSON secure?

The format itself is harmless — it contains no executable code, and JSON.parse cannot execute anything. Two mistakes make it dangerous anyway: reading JSON with eval() (executes arbitrary code) and merging parsed data unchecked into your own objects (prototype pollution). We reproduced the latter: after a single naive merge, every newly created object in the process carried isAdmin: true. The defence is validation against a schema, not suspicion towards the format.

Why is my .json file invalid even though it looks correct?

Three causes are likely. First, an invisible BOM at the start of the file that some Windows editors add — check it with file file.json. Second, typographic quotes inserted by a word processor; they look almost identical and are not. Third, the file is not JSON at all but JSONC — a .json extension is a claim, not a guarantee. On our own server sits a tsconfig.json that TypeScript reads perfectly and JSON.parse rejects with an error at position 129.

Conclusion

A JSON file is unspectacular: text, six data types, a few braces. The reason it still fills an entire article is not its complexity but its quietness.

A syntax error is the friendliest case JSON has to offer — it announces itself. The expensive cases are the others: the number that loses its last digit on the way in. The Map written out as an empty object, whose disappearance nobody notices because the key is still there. The configuration file that works for you and not for the recipient, because Python writes NaN and JavaScript cannot read it. And the object that, after an innocuous-looking merge, suddenly sits inside every other object in the process.

All four share the same pattern: there is no error message because formally nothing is wrong. The result simply is not what somebody meant any more.

The second lesson from our own measurements concerns measuring itself. At 200,000 records, streaming looked worse than loading everything — 0.26 instead of 0.21 seconds, with barely lower memory use. Had we stopped there, the article would have concluded “makes no practical difference”. Only multiplying the data by six revealed what actually happens: one value grows, the other does not. A measurement taken at a single size does not describe a relationship — it describes a point.

If you want to keep going from here: What is an .md file explains the other text format you keep running into, TypeScript typing shows how to make JSON data safe as it is read in, and IT security vulnerabilities places prototype pollution among the larger classes of attack. If you want to see what the log files behind the NDJSON examples look like, that is in our AI crawler log file analysis. And if you are currently setting up a server for these files to land on: Linux server setup is the starting point.