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_id is better than oid).
  • 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