By the end you'll know
- What structured output is, and why code needs it
- How to ask Ollama for JSON, with
format: "json"or a JSON schema - What the schema guarantees, and what it doesn't
- Why a control run is the only way to know what a change did
- Why every field in a structured answer still needs checking
Where you are
So far, the model has answered in plain text: a name, or NONE. That worked, 9 times out of 10. But look at what the code has to assume. That the reply is just a name, with no "Based on the page…" in front. That NONE is spelled exactly NONE. That a name never includes a title. Lesson 5's vague prompt showed what happens when those assumptions break: ten paragraphs, nothing a program could use.
Structured output fixes the shape. Instead of free text, the model answers in JSON (JavaScript Object Notation), a format every programming language can read: named fields, with values that are text, numbers, or null for "nothing". Code doesn't have to guess where the name is. It reads the field called full_name.
That last sentence is the whole lesson, and the results below prove it.
Asking for JSON
Ollama has a request field for this: format. It can take two kinds of value.
"json" tells Ollama the reply must be valid JSON. The prompt says which fields you want; format makes sure whatever comes back can be parsed.
A JSON schema goes further. A schema is a small description of the exact shape you want: which fields, what type each one is, which are required. Here's the one structured.py uses:
SCHEMA = {
"type": "object",
"properties": {
"full_name": {"type": ["string", "null"]},
"title": {"type": ["string", "null"]},
},
"required": ["full_name", "title"],
}
Two fields, full_name and title. Each is either text or null, and both must be there. With a schema in format, Ollama holds the model to that shape while it writes: it can't add a field, leave one out, or put a paragraph where a name should go.
The prompt asks for the same shape in words, and uses null instead of NONE:
Read this web page and find the person currently serving as mayor or acting mayor.
Respond with JSON only, in this shape: {"full_name": string or null, "title": string or null}
Use null for full_name if the page does not name a current mayor or acting mayor.
The request is Lesson 4's, with format added:
payload = {
"model": MODEL,
"prompt": prompt.format(page=page),
"stream": False,
"options": {"temperature": 0},
}
if fmt is not None:
payload["format"] = fmt
And reading the answer is now json.loads, which turns the reply into a Python dictionary, with None for JSON's null:
data = json.loads(raw)
name = data.get("full_name") or "NONE"
The results
structured.py in the practice set runs this against all ten pages, once with format: "json" and once with the schema. Pull the latest practice set (git pull) and run it:
python3 structured.py json schema
== json
ok Cedar Bend full_name Dana Whitfield title Mayor
ok Elm Ridge full_name Carla Mendes title Mayor Pro Tem
MISS Alder Creek full_name None title None
ok Birch Hollow full_name None title None
MISS Fox Run full_name None title None
MISS Juniper Flats full_name Mayor Tom Haskell title acting mayor
MISS Heron Point full_name None title None
ok Quarry Hill full_name None title None
MISS Willow Bend full_name Robert "Bob" Chen title Mayor
ok Stone Mill full_name Ana Silva title Mayor
5 of 10 exactly right, 0 not valid JSON
== schema
ok Cedar Bend full_name Dana Whitfield title Mayor
ok Elm Ridge full_name Carla Mendes title Mayor Pro Tem
MISS Alder Creek full_name None title None
ok Birch Hollow full_name None title None
MISS Fox Run full_name None title None
MISS Juniper Flats full_name Mayor Tom Haskell title acting mayor
MISS Heron Point full_name None title None
ok Quarry Hill full_name None title None
MISS Willow Bend full_name Robert "Bob" Chen title Mayor
ok Stone Mill full_name Ana Silva title Mayor
5 of 10 exactly right, 0 not valid JSON
The good news first: 20 replies, 20 valid JSON objects. Every one parsed. null arrived as a real empty value, not a word to compare. And there's a new field: Elm Ridge's title says Mayor Pro Tem, information that a plain name throws away, and that code could use to flag "this is an acting mayor" for a person to check.
The bad news: 5 out of 10, down from 9 with plain text. Alder Creek, Fox Run and Heron Point came back empty. Juniper Flats came back as Mayor Tom Haskell, title stuck to the name.
One change at a time
It's tempting to conclude that asking for JSON makes the model worse. But that conclusion would be a guess, because two things changed at once: the format option, and the prompt. The JSON prompt is worded differently from Lesson 4's, and asks for a title too. Either change could explain the drop.
When two things change and the result moves, you can't tell which one did it. The fix is a control run: change one thing back, and see whether the result changes with it. structured.py has two more variants for exactly that:
noneis the same JSON prompt, with noformatat all. Ifformatcaused the drop,noneshould do better.narrow-schemais Lesson 4's wording, the one that scored 9, asking only forfull_name, with a one-field schema. If the wording was the problem, this should get back to 9.
python3 structured.py none narrow-schema
== none
ok Cedar Bend full_name Dana Whitfield title Mayor
ok Elm Ridge full_name Carla Mendes title Mayor Pro Tem
MISS Alder Creek full_name None title None
ok Birch Hollow full_name None title None
MISS Fox Run full_name None title None
MISS Juniper Flats full_name Mayor Tom Haskell title acting mayor
MISS Heron Point full_name None title None
ok Quarry Hill full_name None title None
MISS Willow Bend full_name Robert "Bob" Chen title Mayor
ok Stone Mill full_name Ana Silva title Mayor
5 of 10 exactly right, 0 not valid JSON
== narrow-schema
ok Cedar Bend full_name Dana Whitfield title -
ok Elm Ridge full_name Carla Mendes title -
ok Alder Creek full_name Grace Okafor title -
ok Birch Hollow full_name None title -
MISS Fox Run full_name None title -
MISS Juniper Flats full_name Rosa Delgado title -
ok Heron Point full_name Samuel Adeyemi title -
ok Quarry Hill full_name None title -
MISS Willow Bend full_name Robert title -
MISS Stone Mill full_name None title -
6 of 10 exactly right, 0 not valid JSON
Now the picture is clear:
formatdidn't change a single answer.nonegave exactly the same ten answers asjsonandschema. On this model, the prompt alone was enough to get valid JSON every time, and the format option didn't move the score. So the drop came from the prompt.- Asking for JSON at all still cost accuracy. Even with Lesson 4's proven wording,
narrow-schemascored 6, not 9. It found Alder Creek and Heron Point, but lost Stone Mill, cut Willow Bend to a first name, and, worst of all, named Rosa Delgado, the mayor-elect: the same dangerous wrong person Lesson 5 saw.
Compare every variant, and every answer, below:
So what does structured output buy you?
Put the five runs side by side and the honest answer is: reliability, not accuracy.
- What it bought: 40 replies, 40 valid JSON objects. No paragraphs, no "Based on the information provided", no guessing whether NONE is spelled NONE. Code that reads
data["full_name"]won't break on a strange reply. And extra fields liketitlelet the model hand over more than one fact at a time. - What it cost here: accuracy. Plain text scored 9; the best JSON version scored 6. On this small model, asking for an answer in a different shape changed which answers it gave.
- What
formatitself does: it guarantees the shape. On these pages, the 3B model kept to the shape without being forced. On longer or messier pages, or with more fields, it might not, and that's whenformatearns its place: a schema means a reply can never be something your code can't read.
That's not a reason to avoid structured output. It's a reason to test it like any other change. Two things to watch:
- Every field can be wrong. Juniper Flats'
titlesays "acting mayor". Tom Haskell is the mayor; the page says he "continues to serve" until Rosa Delgado is sworn in. The schema made suretitlewas a string. It couldn't make it true. Lesson 8 is about checking values against the page. - More fields can mean more mistakes. The two-field JSON prompt scored lower than the one-field version. Ask for what you'll actually use.
In my real setup
CivicGrid's weekly mayor check uses structured output, with a schema much like the one in this lesson: full_name and title, each a string or null, both required, and format set to the schema. It runs on the bigger qwen2.5:7b, with a longer prompt.
It needs the guarantee. The check runs unattended on about 2,400 cities, every week, with nobody watching. A reply that wasn't valid JSON, on page 1,700, at 3 a.m., would either crash the run or be quietly skipped. With a schema, every reply parses.
Does asking for JSON cost the 7B model accuracy the way it cost the 3B here? I don't know yet. This lesson is the reason I'll find out: Lesson 10 runs the same pages both ways and compares, instead of assuming.
Spot the problem
Three situations. Work out what's wrong before you read the explanation.
Blaming the wrong change. A team switches their extraction script to JSON output and rewrites the prompt in the same change. Accuracy drops, so they switch back to plain text and decide "JSON makes models worse". What did they skip? A control run. Two things changed together, so they can't know which caused the drop. In this lesson, the format option made no difference; the wording did.
Valid but wrong. A script's JSON schema guarantees every reply has full_name and title as strings. The team stops checking the values, since "the schema validates everything". A week later they find people listed as "acting mayor" who aren't. What went wrong? A schema checks the shape, not the facts. "title": "acting mayor" is a perfectly valid string. Values still need checking against the source.
Asking for everything. Someone extends their schema from two fields to twelve: name, title, phone, email, term start, term end and more, "since it's free". Their name accuracy drops. Why might that happen? Every field is more for a small model to get right, and this lesson saw a two-field prompt score lower than a one-field version. Ask for the fields you'll actually use, and test after adding any.
Where you are now
Structured output makes a model answer in JSON, so code reads named fields instead of guessing at free text. Ollama's format takes "json" or a JSON schema, and a schema holds the model to an exact shape. On the practice pages, every one of 40 replies was valid JSON, and the title field added useful detail. But accuracy fell from 9 to 5, and a control run showed it was the prompt, not format, that changed the answers; even the best JSON version scored 6, and named the wrong person once. Structured output buys reliability, not accuracy, so test it like any other change, and check every field. Lesson 8 is about exactly that: catching wrong answers before anyone trusts them.
Check yourself
What does structured output change about a model's answer?
What does setting format to a JSON schema guarantee?
The json and none runs gave identical answers. What does that show?
Why run a control when two things changed at once?
The title field said "acting mayor" for a mayor. What should code do?
When does the format option earn its place?
Key terms
- Structured output: a model's answer in a fixed, machine-readable shape, instead of free text.
- JSON: JavaScript Object Notation, a text format of named fields that every programming language can read.
null: JSON's value for "nothing". Python reads it asNone.format: Ollama's request field for structured output:"json", or a JSON schema.- JSON schema: a description of the exact shape a JSON answer must have: its fields, their types, which are required.
- Control run: a run with one change undone, to find out which change caused a result.
Go deeper
- The practice set (opens in a new tab):
structured.pyhas all four JSON variants. Try a schema with a third field, and see what it does to the score. - Ollama's structured outputs (opens in a new tab) page, with examples of
formatusing a schema. - JSON Schema: Getting Started (opens in a new tab), for writing schemas beyond the two fields here.