Index Ai Academy
Phase 0 · Tools · Working with data

JSON

The text format APIs and LLMs use to exchange structured data.

About 20 minutes · Free lesson

Start this lesson

Why it matters

JSON is the format the internet uses to send structured data. It's also how you talk to LLMs in code. Every request to a model API is a JSON object, and every reply comes back as JSON. When you ask a model for structured output, such as "give me the sentiment and three keywords", you ask for JSON and then read it. Once you're fluent here, API responses stop looking like walls of brackets.

What you'll be able to do

  • Turn JSON text into Python data with json.loads, and back with json.dumps
  • Map each JSON type to its Python equivalent
  • Reach values buried in nested JSON with keys and indexes
  • Format JSON readably and recognize invalid JSON

What it covers

  1. Text that looks like Python data
  2. json.loads: from text to Python
  3. Finding your way through nested JSON
  4. When the JSON is broken
  5. Worked example: reading a model's structured reply
  6. JSON isn't Python, even when it looks it
  7. One level at a time

Text that looks like Python data

Think of a shipping label. It has fixed fields, such as name, street and zip code, in a layout any courier in the world can read. JSON (JavaScript Object Notation) does the same for data. It's a way of writing structured data as plain text that any program can read:

{
  "campaign": "Spring Sale",
  "active": true,
  "budget": 5000,
  "channels": ["search", "email"],
  "owner": null
}

It looks almost exactly like a Python dictionary, and that's no accident. But it's text: a string of characters that any language can read and write. That's why APIs use it. The small differences from Python matter:

JSONPython
object {"key": value}dict
array [1, 2, 3]list
string "text" (double quotes only)str
number 42 or 4.2int or float
true / falseTrue / False
nullNone

JSON also doesn't allow single quotes, comments or a trailing comma after the last item.

json.loads: from text to Python

The json module in the standard library converts between the two. json.loads ("load string") parses JSON text into Python data:

import json

text = '{"campaign": "Spring Sale", "active": true, "budget": 5000, "channels": ["search", "email"], "owner": null}'
data = json.loads(text)

print(type(text))
print(type(data))
print(data["campaign"], data["active"], data["owner"])
print(data["channels"][1])
It runs right here in your browser, with nothing to install.

Before loads, you have one long string, and you can't ask it for the budget. After, you have an ordinary dictionary. true has become True, null has become None, and the array is a list you can index.

json.dumps: from Python to text

json.dumps ("dump string") goes the other way. indent adds line breaks and spacing for people. Without it, you get the compact form that programs send to each other:

import json

report = {"campaign": "Spring Sale", "roas": 5.08, "flags": [], "approved": False, "notes": None}
print(json.dumps(report))
print(json.dumps(report, indent=2))

Note that str(report) would not give you JSON. It uses single quotes and True/None, which other systems will reject. Always use json.dumps when something else needs to read it.

Here's the exact shape of a real request to Claude's Messages API, built as a Python dictionary and turned into JSON. The lessons on APIs show how to send it:

import json

request_body = {
    "model": "claude-opus-5-5",
    "max_tokens": 300,
    "messages": [
        {"role": "user", "content": "Write a subject line for a fall sale."}
    ],
}
print(json.dumps(request_body, indent=2))
Quick check
Which of these is valid JSON?
  1. {"active": True}
  2. {'active': True, 'owner': None}
  3. {"active": true, "owner": null}
  4. {"tags": ["sale", "new",]}
Show the answer

{"active": true, "owner": null}

JSON needs double quotes, lowercase true/false, null instead of None, and no comma after the last item.

Try it now

A typical API response. Click any value to see the Python that reaches it: each step down is one [key] or [index].

Finding your way through nested JSON

Real responses nest, like boxes inside boxes: a dictionary holding a list of dictionaries holding more dictionaries. You reach a value by chaining one lookup per level, from the outside in:

response["data"][0]["metrics"]["revenue"] means: the "data" key (a list), its first item (a dictionary), that item's "metrics" (another dictionary), and its "revenue".

import json

text = """
{"data": [
  {"id": "cmp_101", "name": "Spring Sale - Search", "metrics": {"clicks": 5470, "revenue": 15840}},
  {"id": "cmp_104", "name": "Newsletter Promo - Email", "metrics": {"clicks": 1920, "revenue": 6600}}
], "count": 2}
"""
response = json.loads(text)

total = 0
for campaign in response["data"]:
    print(campaign["name"], campaign["metrics"]["revenue"])
    total += campaign["metrics"]["revenue"]

print("Total revenue:", total)
print("Owner:", response["data"][0].get("owner", "unassigned"))

It prints each campaign's revenue, then Total revenue: 22440 and Owner: unassigned.

The triple quotes let a string span several lines. .get("owner", "unassigned") returns a default instead of raising a KeyError when a key might be missing. That's common in API data, where optional fields are often just left out.

Tip: when you're lost in a big response, print one level at a time, or print the whole thing with json.dumps(response, indent=2) and read its shape before writing any lookups.

Quick check
What does this print?
import json
data = json.loads('{"tags": ["sale", "new"], "meta": {"draft": false}}')
print(data["meta"]["draft"], data["tags"][-1])
  1. False sale
  2. false new
  3. False new
  4. draft tags
Show the answer

False new

After parsing, JSON's false is Python's False, and [-1] picks the last tag.

When the JSON is broken

json.loads is strict. Give it something that isn't valid JSON, and it raises a json.JSONDecodeError. The error tells you where it got confused:

import json
json.loads("{'campaign': 'Spring Sale'}")

Single quotes are the classic cause. Others: a trailing comma, True instead of true, or extra text around the JSON, such as a model replying "Sure! Here's the data: {...}".

For AI work: asking a model for JSON and parsing its reply is one of the most common patterns in LLM apps. Current model APIs offer structured outputs, which guarantee the reply is valid JSON matching a schema you provide. You'll use them in Phase 1. Until then, expect parsing to fail sometimes and handle it.

For files, the module has two siblings without the "s": json.dump writes to a file, and json.load reads from one. You'll also meet JSONL (JSON Lines): one JSON object per line. It's the usual format for datasets of prompts and replies.

Quick check
A model replies Sure! Here's the data: {"score": 4}. What happens when you pass the whole reply to json.loads?
  1. It returns None
  2. It returns {'score': 4}
  3. It raises a JSONDecodeError, because of the extra text before the JSON
  4. It returns the text unchanged
Show the answer

It raises a JSONDecodeError, because of the extra text before the JSON

json.loads needs the text to be JSON and nothing else. Structured outputs, or asking for JSON only, avoid the extra words.

Worked example

Worked example: reading a model's structured reply

You asked a model to classify a customer review and reply in JSON. Its reply arrives as text. Turn it into data and act on it.

import json

model_reply = '{"sentiment": "negative", "score": 0.18, "topics": ["shipping", "refund"], "needs_followup": true}'

review = json.loads(model_reply)

if review["needs_followup"]:
    print("Flag for the support team:", ", ".join(review["topics"]))
print(f"Sentiment: {review['sentiment']} (score {review['score']})")
In the lesson you can change this and run it.

It prints Flag for the support team: shipping, refund, then Sentiment: negative (score 0.18).

The model's output is just a string until json.loads reads it. After that:

  • needs_followup is a real True your if can test,
  • topics is a list you can join,
  • score is a number you could compare or average across thousands of reviews.

That jump from "text a model wrote" to "data your program can act on" is the core move of most LLM features.

JSON isn't Python, even when it looks it

JSON and Python dictionaries look almost the same, and that's the trap. str(data) looks like JSON, but it uses single quotes and writes True and None. Another program will reject it. Always use json.dumps.

import json

data = {"active": True, "coupon": None}
print(str(data))
print(json.dumps(data))

The reverse catches people too. JSON's true and null become Python's True and None once parsed, so compare against those. And text from an API stays plain text until you call json.loads. If reply["name"] complains that string indices must be integers, you're still holding text, not a dictionary.

One level at a time

Lost in nested JSON? Print one level, look at it, and then go one level deeper. Printing as you go beats guessing.

Practice what you just read

  • 7 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Start this lesson

Try a quiz question

Which of these is valid JSON?

  1. {'a': 1}
  2. {"a": 1,}
  3. {"a": true}
  4. {"a": True}

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson