Ask a local model for JSON in plain English and you will eventually get a markdown fence, a friendly preamble, or a trailing comma. Ollama can instead constrain the output to a JSON schema, so the reply always has the shape you declared. Pair that with Pydantic and you get typed data out of a model running on your own machine. This post walks through the setup from Ollama's structured-outputs documentation, then adds the validation loop you still want in production.
The idea
Ollama's chat API accepts a format field. Instead of the string "json", you can pass a full JSON schema. The model's output is then restricted to text that matches it. Because the schema comes from a Pydantic model, the same class that defines the shape also validates the reply.

Setup
You need a running local Ollama server, the Python client and Pydantic, and a model pulled. The docs use gpt-oss for text; any model you have pulled works the same way.

pip install ollama pydantic
ollama pull gpt-ossPlain HTTP first
The raw API call shows what is really happening. The schema goes straight into format:
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"]
}
}'The same thing with Pydantic
In Python you rarely write that schema by hand. Define a model and pass its schema:
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)model_validate_json parses and validates in one step. If the content does not fit the model, Pydantic raises a ValidationError instead of handing you a half-right dictionary.
Two tips from the docs
- Put the schema in the prompt too. The documentation recommends including the JSON schema as a string in the prompt as well as in
format, to ground the model's response. The constraint controls the shape; the prompt helps the model understand what each field means. - Use temperature 0. Setting it to
0makes completions more deterministic, which is what you want for extraction.
Here is a helper that applies both:
import json
from ollama import chat
from pydantic import BaseModel, ValidationError
def extract(model_cls, text, model='gpt-oss'):
schema = model_cls.model_json_schema()
prompt = (
'Extract the data from the text below. '
'Reply only with JSON matching this schema:\n'
+ json.dumps(schema)
+ '\n\nText:\n' + text
)
response = chat(
model=model,
messages=[{'role': 'user', 'content': prompt}],
format=schema,
options={'temperature': 0},
)
return model_cls.model_validate_json(response.message.content)
class Invoice(BaseModel):
vendor: str
total: float
currency: str
print(extract(Invoice, 'Invoice from Acme Ltd, total due 1,250.00 EUR.'))


