Free-form model output is easy to produce and hard to consume — downstream programs must parse, guess formats, and handle surprises. Ollama's structured outputs press a JSON schema directly onto model responses so every reply matches a predefined shape, turning data extraction, image description, and batch processing into deterministic interfaces [1]. One caveat: Ollama's cloud does not support structured outputs yet — the capability lives on the local service [1].
Two levels: json mode and schema mode
Level one is coarse: pass the string "json" in the format field and the model guarantees valid JSON with a structure of its own choosing [1]:
curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "gpt-oss",
"messages": [{"role": "user", "content": "Tell me about Canada in one line"}],
"stream": false,
"format": "json"
}'from ollama import chat
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': 'Tell me about Canada.'}],
format='json'
)
print(response.message.content)import ollama from 'ollama'
const response = await ollama.chat({
model: 'gpt-oss',
messages: [{ role: 'user', content: 'Tell me about Canada.' }],
format: 'json'
})
console.log(response.message.content)Level two is precise: the format field takes a JSON Schema, constraining field names, types, and required keys [1]:
curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "gpt-oss",
"messages": [{"role": "user", "content": "Tell me about Canada."}],
"stream": false,
"format": {
"type": "object",
"properties": {
"name": {"type": "string"},
"capital": {"type": "string"},
"languages": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["name", "capital", "languages"]
}
}'A practical suggestion from the docs: pass the schema as a string inside the prompt as well — grounding the model this way makes responses track expectations more tightly than decoding constraints alone [1].
Under the hood this is constrained decoding: every generated token must fall within what the schema allows, so field order, quote closure, and type validity are guaranteed by the decoding process rather than begged for in the prompt. That is the essential difference from asking for JSON in the prompt, where a stray opening sentence can break the structure at any time. The trade-off is reduced output diversity — creative tasks should skip schemas, while data-interface tasks are exactly its home ground.
The type-safe loop
The real engineering win is defining the schema once and reusing it for both generation and validation. In Python, Pydantic classes do both: model_json_schema() produces the constraint, model_validate_json() validates the reply [1]:
from ollama import chat
from pydantic import BaseModel
class Country(BaseModel):
name: str
capital: str
languages: list[str]
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': 'Tell me about Canada.'}],
format=Country.model_json_schema(),
)
country = Country.model_validate_json(response.message.content)
print(country)In JavaScript, Zod mirrors the pattern: z.toJSONSchema() serializes, and the same object parses the response [1]:
import ollama from 'ollama'
import * as z from 'zod'
const Country = z.object({
name: z.string(),
capital: z.string(),
languages: z.array(z.string()),
})
const response = await ollama.chat({
model: 'gpt-oss',
messages: [{ role: 'user', content: 'Tell me about Canada.' }],
format: z.toJSONSchema(Country),
})
const country = Country.parse(JSON.parse(response.message.content))
console.log(country)The loop converts model output is untrusted into the type system catches it: even if the model drifts off-schema occasionally, validation fails loudly instead of letting bad data flow into business code.
Extraction in practice
The signature use case is pulling structured data out of free text: define the objects to return and let the model populate the fields. Nested models and optional fields both work [1]:
from ollama import chat
from pydantic import BaseModel
class Pet(BaseModel):
name: str
animal: str
age: int
color: str | None
favorite_toy: str | None
class PetList(BaseModel):
pets: list[Pet]
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': 'I have two cats named Luna and Loki...'}],
format=PetList.model_json_schema(),
)
pets = PetList.model_validate_json(response.message.content)
print(pets)The str | None optional fields are the key design: information absent from the text becomes null instead of an invented value. When designing extraction schemas, writing leave-blank-if-unsure into the types beats repeating it in the prompt.
Structured vision
Vision models take the same format parameter, which turns image description into a deterministic interface. Literal enums lock categorical fields, and temperature 0 maximizes determinism [1]:
from ollama import chat
from pydantic import BaseModel
from typing import Literal, Optional
class Object(BaseModel):
name: str
confidence: float
attributes: str
class ImageDescription(BaseModel):
summary: str
objects: list[Object]
scene: str
colors: list[str]
time_of_day: Literal['Morning', 'Afternoon', 'Evening', 'Night']
setting: Literal['Indoor', 'Outdoor', 'Unknown']
text_content: Optional[str] = None
response = chat(
model='gemma4',
messages=[{
'role': 'user',
'content': 'Describe this photo and list the objects you detect.',
'images': ['path/to/image.jpg'],
}],
format=ImageDescription.model_json_schema(),
options={'temperature': 0},
)
image_description = ImageDescription.model_validate_json(response.message.content)
print(image_description)This pattern supports moderation, asset tagging, and photo-search pipelines directly: the objects array carries confidence scores, time_of_day and setting are enums, and downstream code filters on fields instead of parsing prose.
The reliability checklist
The documented advice compresses to three items [1]: define schemas with Pydantic or Zod so one definition covers both constraining generation and validating responses; lower the temperature (say to 0) for more deterministic completions; and through the OpenAI-compatible API, structured outputs activate via response_format. One engineering habit to add: treat schema evolution as an interface change — adding fields is fine, while removing or retyping them deserves a version bump, since every downstream parser depends on that shape [1].
Adapted from the Structured Outputs section of the Ollama documentation[1], released under the MIT License.
Sources
- 1Structured Outputs | Ollama docsOllama docs (MIT)
Cite this page: Structured outputs with Ollama. TellWorld, https://www.tellworld.net/practice/practice-ollama-structured-outputs. Updated 2026-11-14