Skip to content

Home / Glossary / JSON (JavaScript Object Notation)

Tech & IT

JSON (JavaScript Object Notation)

By Sitraka Forler · Lecturer, Durham Business SchoolUpdated 3 October 2026 About this site

A plain-text format for structured data, built from objects, arrays and simple values.

JSON is the usual format for web APIs (application programming interfaces) and many config files, and it maps neatly onto Python dicts and lists. Values are objects, arrays, strings, numbers, true, false or null, and every key is a double-quoted string. Comments, trailing commas, single quotes and NaN (not a number) are all invalid, even though Python's json.dumps writes NaN by default.

The intuition

Two programs that need to exchange data share only one thing: a stream of text. JSON is the agreed way to write a structured value down as text, so the program at the other end can rebuild exactly the same value. It exists because the alternatives fit badly. A CSV file is a flat grid with no way to say that one trade carries a list of prices, and XML wraps every value in an opening and a closing tag, which inflates both the size and the reading effort. JSON keeps the punctuation to a minimum: braces for a named collection, square brackets for an ordered list, and bare literals for everything else. That is why almost every web API answers in it, why so many configuration files are written in it, and why you can open a response in any text editor and understand it without a tool.

The whole format is six value types plus one rule about how they nest. A value is an object, an array, a string, a number, true or false, or null. (The standard spells true and false out separately as literal names, so you will also see the list given as seven.) An object is a collection of key and value pairs between braces, and every key must be a string in double quotes, as in {"ccy": "USD"}. An array is an ordered list between square brackets, and its items need not share a type, as in [101.5, "n/a", null]. The grammar is recursive, and that is where the expressive power comes from: any value inside an object or an array can itself be an object or an array, so one response can carry a list of trades, each holding a list of prices. Nothing else exists. There is no date type, no decimal type, no comment syntax and no way to point at another part of the same document, which is exactly why the format stayed small enough for every language to read it.

Strictness is where people get hurt. Single quotes, a trailing comma after the last item, an unquoted key and a // comment are all rejected, even though the snippet you copied them from was probably legal: single quotes and trailing commas pass in both JavaScript and Python, unquoted keys and // comments pass in JavaScript, and Python writes its own comments with # instead. Python's json module is the usual tool: json.loads turns text into dicts, lists, strings, ints, floats, booleans and None, and json.dumps goes the other way. It bends the rules in one place worth knowing, because by default it writes NaN and Infinity, which no strict reader accepts; pass allow_nan=False when the output leaves your machine. For analysis, pandas takes over: pd.DataFrame on a list of flat records, or pd.json_normalize when the rows sit under a key and you want fields from the level above copied onto each row. When a file grows too large to hold in memory, teams switch to one JSON object per line, a convention called JSON Lines or NDJSON.

Example

{
  "trade_id": "T-1001",
  "currency": "EUR",
  "quantity": 10,
  "prices": [101.5, 102.25],
  "settled": true,
  "comment": null
}

One API response, read into a pandas DataFrame

  1. The response. A GET request to /v1/trades returns status 200 with the header content-type: application/json and this body: { "as_of": "2026-09-18", "base": "EUR", "trades": [ {"trade_id": "T-1001", "ccy": "USD", "quantity": 10, "price": 101.5, "settled": true}, {"trade_id": "T-1002", "ccy": "GBP", "quantity": 4, "price": 250.0, "settled": true}, {"trade_id": "T-1003", "ccy": "USD", "quantity": 25, "price": 98.4, "settled": false, "comment": null} ] } All six value types are visible: the document is an object, "trades" holds an array, "EUR" is a string, 10 and 101.5 are numbers, settled is true or false, and comment is null.
  2. Parse it. import json, then payload = json.loads(text). type(payload) is dict and list(payload.keys()) is ['as_of', 'base', 'trades']. len(payload["trades"]) is 3. The types survive the trip: payload["trades"][0]["quantity"] is the int 10, its "price" is the float 101.5, its "settled" is True, and payload["trades"][2]["comment"] is None.
  3. Build the frame from the list of records, not from the whole response: import pandas as pd, then df = pd.DataFrame(payload["trades"]). df.shape is (3, 6) and df prints as trade_id ccy quantity price settled comment 0 T-1001 USD 10 101.5 True NaN 1 T-1002 GBP 4 250.0 True NaN 2 T-1003 USD 25 98.4 False NaN Rows 0 and 1 never had a comment key at all, and pandas filled the gap the same way it filled row 2's null.
  4. Check the types before trusting anything. df.dtypes gives trade_id object, ccy object, quantity int64, price float64, settled bool and comment float64. That last one surprises people: the only value ever supplied was null, which became NaN, and a column with nothing but missing values gets the float default rather than a text dtype.
  5. Do the arithmetic. df["notional"] = df["quantity"] * df["price"] gives 10 x 101.5 = 1015.0 for T-1001, 4 x 250.0 = 1000.0 for T-1002 and 25 x 98.4 = 2460.0 for T-1003. The total is df["notional"].sum() = 1015.0 + 1000.0 + 2460.0 = 4475.0.
  6. Filter on the boolean column, which needs no conversion because JSON true and false became a real bool dtype: df.loc[df["settled"], "notional"].sum() = 1015.0 + 1000.0 = 2015.0. The unsettled remainder is 4475.0 - 2015.0 = 2460.0, which is T-1003 on its own.
  7. Keep the metadata from the level above. flat = pd.json_normalize(payload, record_path="trades", meta=["as_of", "base"]) returns a frame of shape (3, 8): the same six columns plus as_of and base repeated on every row. Note that flat["as_of"] has dtype object, because JSON has no date type, so you still need pd.to_datetime(flat["as_of"]) to get datetime64[ns].
  8. Break it on purpose. Add one comma after the last trade object: { "trades": [ {"trade_id": "T-1001", "quantity": 10}, ] } json.loads on that raises json.JSONDecodeError: Expecting value: line 4 column 3 (char 62). The same stray comma inside an object instead of an array reads Expecting property name enclosed in double quotes. Either way the position given is where the parser gave up, which is the next thing it found after the comma, here the closing bracket on line 4, so look just before it.

Parsing is one line; the real work is picking the key that holds your rows and then reading df.dtypes, because nulls, absent keys and dates never arrive as the types you assumed.

Common pitfalls

  • Retyping a Python dict by hand into a .json file. True, False, None and single quotes are all invalid; the JSON spellings are true, false, null and double quotes throughout, including around every key. Let json.dumps do the conversion instead of editing the text yourself.
  • The trailing comma and the comment. A comma after the last item passes in modern JavaScript and in Python source, and a // or /* */ comment passes in JavaScript, where Python would write #. Every JSON parser rejects all of them, which is why config files that need comments reach for YAML, TOML or a JSON dialect such as JSONC. Strip them, and remember the error position points at the next thing the parser found, not at the comma itself.
  • Letting NaN out of Python. json.dumps writes NaN and Infinity by default even though no strict reader accepts them, so a file that round-trips perfectly in pandas can fail the moment another team parses it. Pass allow_nan=False to fail loudly at write time, or replace missing values with null first.
  • Handing the whole response to pandas. On a body shaped {"as_of": ..., "base": ..., "trades": [...]}, pd.DataFrame(payload) quietly returns a (3, 3) frame whose trades column holds three dictionaries, so nothing is ever split into columns. Print list(payload.keys()) first, pass the record list itself, and use pd.json_normalize when you also want fields from the level above.
  • Sending identifiers as numbers. The grammar sets no limit on a number's size, and Python's json parser does keep whole numbers exact, but JavaScript loads every number into a 64-bit float and is exact only up to 2^53 - 1 = 9007199254740991, so a long account or trade number that crosses a browser or a JavaScript service can come back with its last digits changed. Send identifiers as strings and keep numbers for things you will actually calculate with.

Frequently asked questions

What is JSON in simple terms?

JSON (JavaScript Object Notation) is a way of writing a structured value down as ordinary text, so one program can send it to another and the other rebuilds the same value. Objects sit in braces, arrays in square brackets, and the rest is strings, numbers, true or false, and null. Most web APIs reply in JSON, and you can read one in any text editor without a special tool. Despite the name it is not tied to JavaScript; every mainstream language reads and writes it.

What is an example of JSON?

A small one is {"ccy": "USD", "quantity": 10, "price": 101.5, "settled": true, "comment": null}. Every key is a string in double quotes, and a value can itself be a container, as in "prices": [101.5, 102.25]. The whole document is one value, in practice almost always an object or an array of objects. The file extension is .json and the media type is application/json.

What is the difference between JSON and CSV?

CSV is a flat grid of rows and columns that carries no type information and cannot nest, so every field is text until the reader guesses otherwise. JSON marks each value as a number, string, boolean or null, and lets one value contain others, so a trade can hold a list of prices. CSV stays smaller and loads faster for a plain rectangular table, which is why exports and warehouses still use it; JSON wins when records are nested or their fields vary. Neither format has a date type, so dates arrive as text in both.

Is JSON the same as a Python dictionary?

No. A dict is a live object in memory, while JSON is text, and json.loads and json.dumps convert between them. The notations also differ: JSON writes true, false and null where Python writes True, False and None, and JSON demands double quotes around every key. That is why pasting a printed dict straight into a .json file usually fails to parse.

Why do I get 'Expecting property name enclosed in double quotes'?

The parser reached a point where a key should begin and found something else, most often a trailing comma after the last pair, a single-quoted or unquoted key, or a comment. The line and column in the message mark where it gave up, which is the next thing it found after the real mistake, so check just before that position. Delete the extra comma, switch to double quotes, and remove any // or /* */ comments.

How do I read JSON into pandas?

If the records are already a flat list, parse then pass that list: payload = json.loads(text), then df = pd.DataFrame(payload["trades"]). If the rows are nested, or you need fields from the level above repeated on each row, use pd.json_normalize(payload, record_path="trades", meta=["as_of", "base"]). pd.read_json can load a file or URL in one call but is fussy about the layout it expects. Whichever route you take, check df.dtypes straight afterwards.

Test yourself

Practise JSON (JavaScript Object Notation) hands-on - free, in your browser

Related terms