JSON
The text format APIs and LLMs use to exchange structured data.
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
- Text that looks like Python data
- json.loads: from text to Python
- Finding your way through nested JSON
- When the JSON is broken
- Worked example: reading a model's structured reply
- JSON isn't Python, even when it looks it
- 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:
| JSON | Python |
|---|---|
object {"key": value} | dict |
array [1, 2, 3] | list |
string "text" (double quotes only) | str |
number 42 or 4.2 | int or float |
true / false | True / False |
null | None |
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])
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))
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.
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.
import json data = json.loads('{"tags": ["sale", "new"], "meta": {"draft": false}}') print(data["meta"]["draft"], data["tags"][-1])
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.
Sure! Here's the data: {"score": 4}. What happens when you pass the whole reply to json.loads?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: 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']})")
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_followupis a realTrueyourifcan test,topicsis a list you can join,scoreis 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
Try a quiz question
Which of these is valid JSON?