Embercode
✕
Data Formats and TextLesson 1 of 7

45 min read

JSON: objects, arrays and parsing

Why this matters

Programs often need to pass data to each other. A weather app asks a server for the temperature. A shop sends an order to a payment service. A tool reads a settings file to learn which folder to watch. Each of these needs an agreed way to write data as plain text, so that the sender and the receiver can both read it.

JSON (JavaScript Object Notation, said "jay-son") is the most common text format for this job. Almost every web API sends and receives JSON. Many settings files use it, and the exports and logs you will meet as a developer often do too.

JSON is strict. A reader does not guess what you meant. If one comma is in the wrong place, the whole file is refused. This lesson teaches the rules, shows how to write JSON that a reader accepts, and shows how to turn JSON text into data your code can use, and back again. The next lesson in this module covers CSV, a format for tables.

The idea: plain text with a fixed shape

Text is just characters. JSON adds a fixed shape to text. Every piece of JSON is one of six kinds of value.

KindWhat it looks likeExample
Stringtext inside double quotes"Dune"
Numberdigits, with an optional minus sign and decimal point1965 or -3.5
Booleanthe word true or the word false, in lower casetrue
Nullthe word null, which means "no value"null
Arrayan ordered list inside square brackets[1, 2, 3]
Objecta set of named values inside curly braces{"title": "Dune"}

An object is a set of pairs. Each pair has a key, which is always a name in double quotes, then a colon, then a value. The pairs are separated by commas. An array is a list. Its items are separated by commas. Both can hold any of the six kinds, including other objects and arrays. That is what lets JSON describe a whole order with its customer, its items and its address in one piece of text.

Example 1: one object

Here is a small object that describes a book.

{
  "title": "Dune",
  "year": 1965,
  "available": true,
  "tags": ["science fiction", "classic"],
  "rating": null
}

Read it one line at a time. The first character is an opening curly brace, which starts an object. The first pair is the key "title", then a colon, then the string value "Dune". Each pair after that follows the same pattern. The line "tags" holds an array with two strings. The last pair, "rating", has the value null, so the field exists but has no value yet. The closing curly brace ends the object.

Notice the commas. There is a comma after every pair except the last one. This rule matters a lot, and you will see it again in the mistakes section.

A program reads the text with a parser. A parser is a piece of code that checks the text against the rules and turns it into data your language understands. In Python the function is json.loads. The s at the end stands for "string", because it reads from text.

import json
 
book_text = """{
  "title": "Dune",
  "year": 1965,
  "available": true,
  "tags": ["science fiction", "classic"],
  "rating": null
}"""
 
book = json.loads(book_text)
print(type(book).__name__, len(book))
print(book["title"])
print(book["tags"][1])
print(book["rating"])
print(book["available"])

Output:

dict 5
Dune
classic
None
True

The parser turned the object into a Python dict, which is a set of keys and values that you can look up by name. The array became a list, so book["tags"][1] picks the second item, counting from zero. The null became None, and true became True. Python uses capital letters for these words, but the text itself must use lower case.

Here is the mapping between JSON and the two languages in this lesson.

JSONPythonJavaScript
objectdictplain object
arraylistarray
stringstrstring
numberint or floatnumber
true or falseTrue or Falsetrue or false
nullNonenull

Example 2: an array of objects

Most APIs send a list of records. A record is one object, and the list holds many of them. Each record in a list usually has the same keys.

[
  {"name": "Asha", "age": 21},
  {"name": "Ravi", "age": 19}
]

The outer square brackets make this an array. Each item is an object. The braces and the pairs inside them work exactly as in example 1. Commas separate the items, and there is no comma after the last one.

import json
 
people_text = """[
  {"name": "Asha", "age": 21},
  {"name": "Ravi", "age": 19}
]"""
 
people = json.loads(people_text)
print(type(people).__name__, len(people))
for person in people:
    print(person["name"], person["age"])

Output:

list 2
Asha 21
Ravi 19

The for loop visits each object in turn. The variable person holds one object at a time, so person["name"] reads that record's name. This pattern, a loop over a list of objects, is the most common thing you will do with JSON.

Example 3: nested data

Real data is deeper. An order contains a customer, which is an object, and a list of items, where each item is also an object. This is called nesting. Each level sits inside the level above it.

{
  "order_id": 1042,
  "customer": {"name": "Meera", "email": "meera@example.com"},
  "items": [
    {"sku": "BK-101", "qty": 2, "price": 349.5},
    {"sku": "PN-900", "qty": 1, "price": 120}
  ],
  "gift": false,
  "note": null
}

To reach a value deep inside, you follow the path one step at a time. In Python, order["customer"] gives the inner object, and order["customer"]["email"] gives the email. For the items, order["items"] gives the list, order["items"][0] gives the first item, and order["items"][0]["sku"] gives its code.

The next program adds up the order. Each item costs its price times its quantity.

import json
 
order_text = """{
  "order_id": 1042,
  "customer": {"name": "Meera", "email": "meera@example.com"},
  "items": [
    {"sku": "BK-101", "qty": 2, "price": 349.5},
    {"sku": "PN-900", "qty": 1, "price": 120}
  ],
  "gift": false,
  "note": null
}"""
 
order = json.loads(order_text)
print(order["customer"]["email"])
print(order["items"][0]["sku"])
total = sum(item["qty"] * item["price"] for item in order["items"])
print("total", total)
print(type(order["items"][1]["price"]).__name__, order["items"][1]["price"])
print(type(order["items"][0]["price"]).__name__, order["items"][0]["price"])

Output:

meera@example.com
BK-101
total 819.0
int 120
float 349.5

The total is 819.0 with a decimal point, even though most of the numbers are whole. The reason is the price 349.5. It has a decimal part, so the parser made it a float. The price 120 has no decimal point, so it became an int. When Python adds a float to anything, the result is a float. The program prints what the data holds, and the output shows the types, which is a good habit when a number looks wrong.

Writing valid JSON by hand

When you write JSON yourself, follow these rules. Each one comes from a real way that a reader rejects a file.

  1. Use double quotes for every string and every key. Single quotes are not allowed.
  2. Every key is a string in double quotes. A key cannot be a bare word.
  3. Put a comma between items, but never after the last item in an object or array.
  4. JSON has no comments. Two slashes, a hash sign and a note inside the file are all errors.
  5. Write true, false and null in lower case.
  6. Numbers cannot have a plus sign, cannot start with a zero unless the number is 0 itself, and cannot start or end with a bare decimal point.
  7. Inside a string, a double quote must be written as \". A backslash must be written as \\.
  8. The file holds exactly one value. Two objects one after another are not valid.

The tests below show the rules in action. Each line is a separate test, and the output is what Python 3.14 printed.

import json
 
tests = {
    "leading zero": "007",
    "plus sign": "+1",
    "no digit before point": ".5",
    "exponent": "1e3",
    "escaped quote": '{"quote": "She said \\"hi\\""}',
}
for label, text in tests.items():
    try:
        print(label, "->", repr(json.loads(text)))
    except json.JSONDecodeError as err:
        print(label, "->", err)

Output:

leading zero -> Extra data: line 1 column 2 (char 1)
plus sign -> Expecting value: line 1 column 1 (char 0)
no digit before point -> Expecting value: line 1 column 1 (char 0)
exponent -> 1000.0
escaped quote -> {'quote': 'She said "hi"'}

Notice that 1e3 is accepted and read as 1000.0. JSON allows an exponent, which means "times ten to the power of", so 1e3 is a thousand. This is valid, but most people do not write numbers this way, so you will rarely see it.

Spotting a trailing comma

A trailing comma is a comma after the last item of an object or array. It is the most common JSON mistake, because many programming languages allow it. JavaScript objects and Python lists accept a trailing comma, so it looks correct to a developer who has not yet learned the JSON rules.

Here is a one-line object with one trailing comma.

{"name": "Asha", "age": 21,}

Python reports the problem like this:

Illegal trailing comma before end of object: line 1 column 27 (char 26)

The message names the problem exactly, and it points to the comma, which is at column 27. Node.js, the JavaScript runtime, reports the same file differently:

SyntaxError: Expected double-quoted property name in JSON at position 27 (line 1 column 28)

Node points at the closing brace, which is the character just after the comma. Both messages are right about the place, but they point at different characters. When a message refers to a position, look at the characters just before it.

A file that spans several lines is easier to fix. Here is a file with the same problem on line 4.

{
  "name": "Asha",
  "age": 21,
  "city": "Pune",
}

Python reads it and prints:

error: Illegal trailing comma before end of object: line 4 column 17 (char 49)

Line 4 is "city": "Pune",. Column 17 is the comma after "Pune". Delete that comma, and the file is valid. You can find the line with a text editor's line numbers, and many editors flag a trailing comma as you type.

Reading and writing JSON in code

A parser does two jobs. It reads JSON text into data, and it writes data back out as JSON text. In Python the functions are these.

FunctionInputOutput
json.loads(text)a stringdata
json.load(file)an open filedata
json.dumps(data)dataa string
json.dump(data, file)data and an open filenothing, the text is written to the file

The next program writes a file, then reads it back. __file__ is the path of the script itself, so the file is saved in the same folder as the script.

import json
import os
 
folder = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(folder, "student.json")
 
student = {"name": "Asha", "age": 21, "courses": ["maths", "python"]}
with open(path, "w", encoding="utf-8") as f:
    json.dump(student, f, indent=2)
 
with open(path, encoding="utf-8") as f:
    print(f.read())
 
with open(path, encoding="utf-8") as f:
    back = json.load(f)
print(back == student, back["courses"][0])

Output:

{
  "name": "Asha",
  "age": 21,
  "courses": [
    "maths",
    "python"
  ]
}
True maths

The indent=2 argument makes the file easy to read by putting each item on its own line, indented two spaces. Without it, the whole object goes on one line. Both forms are valid JSON, and a parser reads them the same way. The encoding="utf-8" part tells Python which character encoding to use when it writes the bytes to disk. The neighbouring lesson on Unicode explains why this matters.

Two more json.dumps options come up often. sort_keys=True writes the keys in alphabetical order, which makes two files easy to compare. Without it, Python keeps the keys in the order they were added, and it keeps that order when it reads the text again.

import json
 
print(json.dumps({"b": 2, "a": 1}, sort_keys=True))
print(json.dumps({"a": None, "b": True, "c": 1.0}))

Output:

{"a": 1, "b": 2}
{"a": null, "b": true, "c": 1.0}

In JavaScript, the two functions are JSON.parse and JSON.stringify. The word "stringify" means "turn into a string". The third argument of JSON.stringify is the indent width, and the middle argument is for filtering, which you can leave as null.

const bookText = `{
  "title": "Dune",
  "year": 1965,
  "available": true,
  "tags": ["science fiction", "classic"],
  "rating": null
}`;
 
const book = JSON.parse(bookText);
console.log(typeof book, Array.isArray(book), book.title, book.tags[1], book.rating);
console.log(JSON.stringify(book));
console.log(JSON.stringify(book, null, 2));

Output:

object false Dune classic null
{"title":"Dune","year":1965,"available":true,"tags":["science fiction","classic"],"rating":null}
{
  "title": "Dune",
  "year": 1965,
  "available": true,
  "tags": [
    "science fiction",
    "classic"
  ],
  "rating": null
}

Notice that JSON.stringify with no indent argument writes the text with no spaces at all. The text is valid either way. Pick the form that a person will read, or the compact one for a network request where every byte counts.

Common mistakes

A single quote where a double quote belongs. People who write Python often type {'name': 'Asha'}, because Python accepts single quotes. JSON does not. Python reports:

Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Node reports the same mistake as SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2). Both point at the first single quote.

A comment. JavaScript and Python programs use // or # for comments, and some config formats allow them. JSON does not. This input:

{"name": "Asha" // the student
}

fails in Python with:

Expecting ',' delimiter: line 1 column 17 (char 16)

The parser reached the // where it expected a comma, and it stopped there. Remove the comment, or move the note into a field with a name, such as "note": "the student".

Capital letters in true, false or null. The text {"ok": True} fails with Expecting value: line 1 column 8 (char 7) in Python. Node says SyntaxError: Unexpected token 'T', "{"ok": True}" is not valid JSON. Both messages point at the capital T.

An empty file. An empty string is not a value. Python says Expecting value: line 1 column 1 (char 0), and Node says SyntaxError: Unexpected end of JSON input. Both usually mean the file was not saved, or the program wrote nothing before it tried to read.

Two objects one after another. {"a": 1} {"b": 2} looks like two records, but the file holds one value. Python reports Extra data: line 1 column 10 (char 9). The fix is to put the two objects in an array: [{"a": 1}, {"b": 2}]. This is also the most common form of a JSON Lines file, where each line holds one object and the parser reads one line at a time.

Missing comma between pairs. {"a": 1 "b": 2} fails with Expecting ',' delimiter: line 1 column 9 (char 8). The parser sees a value followed by a new key, and it expected a comma.

NaN and Infinity. Python's json.dumps writes the number NaN (not a number) without an error, and the result is not valid JSON:

import json
 
print(json.dumps({"score": float("nan")}))

Output:

{"score": NaN}

A strict reader rejects that text. Node says SyntaxError: Unexpected token 'N', "{"score": NaN}" is not valid JSON. Clean the number before you write it, for example by storing null for a missing score.

Silent loss in JavaScript. JSON.stringify drops some values without a warning. A property set to undefined or to a function disappears, and NaN and Infinity turn into null. The next program shows it.

console.log(JSON.stringify({ x: 1, y: undefined, z: [undefined] }));
console.log(JSON.stringify({ a: undefined, b: NaN, c: Infinity, d: () => 1, e: null }));

Output:

{"x":1,"z":[null]}
{"b":null,"c":null,"e":null}

The key y vanished, and the array item undefined became null. Nothing reported an error. If a value must survive a round trip, set it to a real value or to null before you call JSON.stringify.

Dates. JSON has no date type. A Date in JavaScript is written as a string:

console.log(JSON.stringify({ when: new Date(0) }));

Output:

{"when":"1970-01-01T00:00:00.000Z"}

When you read it back, the value is a string, not a date. You must convert it yourself. The neighbouring lesson on dates and time zones covers this in full.

Circular structures. An object that contains a reference to itself has no finite JSON form. Node reports TypeError: Converting circular structure to JSON when you try. Python reports a similar error for objects it cannot write, such as TypeError: Object of type object is not JSON serializable, which you get from json.dumps({"when": object()}).

Practice

Exercise 1. Write a valid JSON object for a book with a title (text), an author (text), a pages number, and a genres list with two items.

Show the answer

{
  "title": "Dune",
  "author": "Frank Herbert",
  "pages": 412,
  "genres": ["science fiction", "classic"]
}

There is no comma after "genres", and every key is in double quotes. Passing the text to json.loads in Python returns a dict with four keys and raises no error.

Exercise 2. Find every mistake in this text, and explain what Python reports for it first:

{'name': 'Asha', 'skills': ['python', 'sql',],}

Show the answer

There are three kinds of mistake. Every single quote must be a double quote. The comma after 'sql' is a trailing comma inside the array. The comma after the closing bracket of the array is a trailing comma inside the object. Python stops at the first single quote and reports Expecting property name enclosed in double quotes: line 1 column 2 (char 1). Fixing only that one character would reveal the other two errors.

The corrected text is:

{"name": "Asha", "skills": ["python", "sql"]}

Exercise 3. The text below holds three records. Write a Python program that reads it and prints the name of the record whose id is 3.

text = '[{"id": 1, "name": "A"}, {"id": 2, "name": "B"}, {"id": 3, "name": "C"}]'

Show the answer

import json
 
text = '[{"id": 1, "name": "A"}, {"id": 2, "name": "B"}, {"id": 3, "name": "C"}]'
items = json.loads(text)
print(items[2]["name"])

Output:

C

The third record is at index 2, because counting starts at zero. A more careful version searches for the matching id instead of trusting the position, using for item in items: if item["id"] == 3: print(item["name"]). That version still works if the list is reordered.

Exercise 4. Write a Python program that prints this dict as JSON, with four spaces of indent and the keys in alphabetical order: {"b": 2, "a": [1, None]}.

Show the answer

import json
 
print(json.dumps({"b": 2, "a": [1, None]}, indent=4, sort_keys=True))

Output:

{
    "a": [
        1,
        null
    ],
    "b": 2
}

The key a comes before b because of sort_keys=True. Python writes None as null in the output.

Exercise 5. What does JSON.stringify print for { x: 1, y: undefined, z: [undefined] }? Explain why.

Show the answer

It prints {"x":1,"z":[null]}. The property y is left out because its value is undefined, which has no JSON form. Inside the array, undefined cannot be left out without moving the other items, so JSON writes it as null. Nothing is reported, so the data loss is silent.

Summary

  • JSON holds six kinds of value: strings, numbers, booleans, null, arrays and objects, and objects and arrays can nest.
  • Use double quotes for strings and keys, lower case for true, false and null, and never put a comma after the last item.
  • JSON has no comments, no single quotes and no trailing commas, and a file holds exactly one value.
  • Python reads text with json.loads, and JavaScript reads it with JSON.parse. Both return data your code can use.
  • Python's json.dumps writes NaN and JavaScript's JSON.stringify drops undefined, so check values before you write them out.
  • When a parser reports a line and column, look at the characters just before that spot to find the mistake.
Check yourself: JSON checks (8 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.