Contents
Ollama Structured Outputs Tutorial: Return JSON in a Fixed Format
When you integrate a model into a program, you often need more than a block of text. You need data your program can read directly. For example, you might extract a name, order number, and issue type from a customer complaint.
Ollama's format parameter can force the model's output to match the JSON format you specify.
The simplest option: request JSON
curl http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [{ "role": "user", "content": "List three cities in Taiwan and their populations. Respond in JSON." }],
"format": "json",
"stream": false
}'
The output is guaranteed to be valid JSON, but the model chooses the field names and structure, which may vary each time.
Specify a JSON Schema
Put a JSON Schema in format to define the fields precisely:
curl http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [{ "role": "user", "content": "Describe Taipei City" }],
"format": {
"type": "object",
"properties": {
"city": { "type": "string" },
"population": { "type": "integer" },
"landmarks": { "type": "array", "items": { "type": "string" } }
},
"required": ["city", "population", "landmarks"]
},
"stream": false
}'
message.content will be a JSON string that matches the format.
Python: define a format with Pydantic
from ollama import chat
from pydantic import BaseModel
class Complaint(BaseModel):
customer_name: str
order_id: str | None
category: str # For example: shipping, quality, refund, other
urgency: int # 1~5
summary: str
email = """
Hello, I'm Jane Smith. The box of the hair dryer from order A20261012-88 was damaged when it arrived,
and the dryer won't turn on at all when plugged in. I'm traveling next week, so this is urgent. Please process an exchange or refund as soon as possible. Thank you.
"""
response = chat(
model="qwen3:8b",
messages=[
{"role": "system", "content": "Extract information from the customer complaint. category must be one of: shipping, quality, refund, other."},
{"role": "user", "content": email},
],
format=Complaint.model_json_schema(),
options={"temperature": 0},
)
data = Complaint.model_validate_json(response.message.content)
print(data)
print(data.urgency)
customer_name='Jane Smith' order_id='A20261012-88' category='quality' urgency=4 summary='Hair dryer box was damaged and the dryer will not turn on; requests prompt exchange or refund'
4
model_validate_json validates the types again to ensure your program gets valid data.
Restrict choices with Literal or Enum
from typing import Literal
class Complaint(BaseModel):
category: Literal["shipping", "quality", "refund", "other"]
This limits the model to one of these four values, which is more reliable than describing the choices in the prompt.
JavaScript: define a format with Zod
npm install ollama zod
import ollama from "ollama";
import { z } from "zod";
const Product = z.object({
name: z.string(),
price: z.number(),
tags: z.array(z.string()),
});
const response = await ollama.chat({
model: "qwen3:8b",
messages: [{ role: "user", content: "This iPhone case costs 590 dollars, is shockproof and slim, and supports MagSafe." }],
format: z.toJSONSchema(Product),
});
const product = Product.parse(JSON.parse(response.message.content));
console.log(product);
(Zod 4 includes z.toJSONSchema; with older Zod 3, you can use the zod-to-json-schema package.)
Example: batch classification
from typing import Literal
from ollama import chat
from pydantic import BaseModel
class Review(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
topics: list[str]
reviews = [
"The food was delicious, but we waited almost an hour.",
"The staff were very friendly. I'll come back again!",
"The price was average, and the food was average.",
]
for text in reviews:
r = chat(
model="qwen3:8b",
messages=[{"role": "user", "content": f"Analyze this review: {text}"}],
format=Review.model_json_schema(),
options={"temperature": 0},
)
print(text, "→", Review.model_validate_json(r.message.content))
Extract data from images
With a vision model, you can extract structured data from invoices, business cards, and screenshots:
class BusinessCard(BaseModel):
name: str
company: str | None
title: str | None
phone: str | None
email: str | None
r = chat(
model="gemma3",
messages=[{"role": "user", "content": "Extract the information on this business card", "images": ["./card.jpg"]}],
format=BusinessCard.model_json_schema(),
options={"temperature": 0},
)
print(BusinessCard.model_validate_json(r.message.content))
Tips for improving accuracy
- Set temperature to 0 for more consistent output.
- Describe the format and meaning of the fields in the prompt too; don't rely on the schema alone. For example: "urgency is an integer from 1 to 5, where 5 is the most urgent."
- Choose clear, descriptive field names (
order_idis better thanoid). - Make fields that may have no data nullable (
str | None) so the model doesn't invent content just to fill them. - Structured outputs guarantee the format, not the accuracy of the content. Validate important data.
Updated October 12, 2026