Ollama Structured Outputs: Fix JSON Schema Validation Errors

ollamajson-schemapythontroubleshootinglocal-ai

TL;DR: For local Ollama extraction, pass a schema in format, decode the response envelope, then validate the generated content before using it. Valid JSON can still contain the wrong fields or an invented answer. Start with non-streaming requests and a small contract that your application can reject explicitly.

What you’ll be able to do:

  • Distinguish transport parsing errors from invalid extracted records.
  • Send a schema with a local request without adding a Python SDK.
  • Reproduce missing-field and wrong-type failures without running a model.

Honest take: Keep the validator outside the model; a fluent explanation is never a substitute for a record your code actually accepts.

Why does valid Ollama JSON still fail my application?

Ollama returning JSON and your application accepting a record are different checks. A response can parse successfully while missing the key your next function reads. The following deliberately invalid extraction record demonstrates the problem:

import json

record = json.loads('{"device": "office-pc"}')
print(record["status"])

Python raises KeyError: 'status'. There is no broken JSON syntax to repair. The application expected a field that the object did not contain. Repeating “return valid JSON” in the prompt does not specify what that missing field should be.

Use this distinction when deciding where to investigate:

FailureWhat it establishesFirst useful action
HTTP error before a usable responseThe request did not complete normallyRead the status and error body
Outer JSON cannot be decodedThe response is not the document your parser expectsInspect transport format and response type
KeyError reading the envelopeCode expects a different endpoint’s response shapeMatch the parser to the endpoint
Generated content cannot be decodedThe extracted answer is not valid JSON textInspect complete content and request format
Decoded record fails validationJSON syntax passed, application contract failedReject the record and inspect the failing field
Record passes but contradicts the inputStructural checks passed, extraction is wrongCompare the values with the original evidence

If your error is JSONDecodeError: Extra data on the entire HTTP response, use the separate Ollama streaming parser fix. This article starts after you can reliably obtain one completed response.

Verification scope, November 4, 2026: the Python fixture and rejection checks below were run locally. The HTTP request follows the cited Ollama API documentation; we did not run a model, measure extraction accuracy, or benchmark GPU performance for this article.

Which Ollama field should receive the schema?

For Ollama’s native chat endpoint, put the JSON Schema object in the request’s format field. The official structured-output guide documents both format: "json" and a schema object, recommends including the schema in the prompt, and recommends validating the returned content. It also states that Ollama cloud does not currently support structured outputs. This walkthrough targets an already installed local model.

The schema below is our example application contract. It describes a short device-status extraction task, not a built-in Ollama data type:

SCHEMA = {
    "type": "object",
    "properties": {
        "device": {"type": "string"},
        "status": {
            "type": "string",
            "enum": ["online", "offline", "unknown"]
        },
        "evidence": {"type": "string"}
    },
    "required": ["device", "status", "evidence"],
    "additionalProperties": False
}

JSON Schema’s object reference distinguishes defining properties from requiring them. Listing status inside properties is not enough to make its presence mandatory; that is what required does. additionalProperties: false closes this particular object to unexpected keys. These are separate choices, so review both when a model’s reply looks sensible but your consumer refuses it.

The enum reference defines how to restrict a value to a fixed set. Here, unknown is intentional. A note saying only “office-pc was inspected” should not force the model to invent whether the computer was reachable. Adding an honest uncertainty state gives the application something explicit to route for review.

Do not copy these status labels into a different domain automatically. A support-ticket classifier might need a category and confidence policy; an inventory importer might need a serial number and a lookup against known assets. Start from what the receiving application can safely use, then design the fields.

How do I send a local request without installing an SDK?

Python’s standard library can send the native chat request. Save the schema above and the following code in the same file, replacing the model placeholder with the exact local tag shown by ollama list:

import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen

note = "office-pc is offline. The monitor did not receive a heartbeat."
payload = {
    "model": "REPLACE_WITH_YOUR_LOCAL_MODEL_TAG",
    "messages": [{
        "role": "user",
        "content": (
            "Extract a device status from the note. "
            "Use unknown if the status is not stated. "
            "Copy a supporting passage into evidence. "
            "Return only JSON matching this schema: "
            + json.dumps(SCHEMA)
            + "\nNote: " + note
        )
    }],
    "format": SCHEMA,
    "stream": False,
    "options": {"temperature": 0}
}
request = Request(
    "http://localhost:11434/api/chat",
    data=json.dumps(payload).encode("utf-8"),
    headers={"Content-Type": "application/json"},
    method="POST"
)
try:
    with urlopen(request, timeout=120) as response:
        envelope = json.load(response)
except HTTPError as exc:
    detail = exc.read().decode("utf-8", errors="replace")
    raise RuntimeError(f"Ollama HTTP {exc.code}: {detail}") from exc

if "error" in envelope:
    raise RuntimeError(envelope["error"])
if envelope.get("done") is not True:
    raise ValueError("generation did not finish")
record = json.loads(envelope["message"]["content"])
print(record)

The timeout is an example client setting, not a measured response time or an Ollama default. Change it to suit your application. Connection failures and timeouts should stop this example rather than producing an empty “successful” record.

The chat endpoint reference documents the request and response fields used here. The generate endpoint instead places generated text in response. If you switch endpoints, change that final lookup too. Copying message.content code into a generate client is a parser bug, regardless of how well the model follows instructions.

Notice the two decoding steps: first the HTTP envelope, then the generated string inside it. Printing the entire envelope is useful during debugging, but passing it directly to a validator for device records is wrong. It contains API metadata rather than the expected device, status, and evidence fields.

A possible accepted record is shown below. It is a fixture for the tutorial, not a captured model response:

{"device":"office-pc","status":"offline","evidence":"office-pc is offline."}

How can I reject bad records before they reach my automation?

Place an application validator between parsing and any action. For this tiny example, explicit Python checks keep the contract readable and avoid an additional dependency:

def validate_record(value, note):
    expected = {"device", "status", "evidence"}
    if not isinstance(value, dict) or set(value) != expected:
        raise ValueError("expected exactly device, status, evidence")
    if any(not isinstance(value[key], str) for key in expected):
        raise ValueError("all record fields must be strings")
    if not value["device"].strip():
        raise ValueError("device must not be empty")
    if value["status"] not in {"online", "offline", "unknown"}:
        raise ValueError("status is not an allowed value")
    if not value["evidence"].strip() or value["evidence"] not in note:
        raise ValueError("evidence must be copied from the note")
    return value

After the request, call validate_record(record, note) before storing or acting on the result. This function implements only our small application contract. It is not a general JSON Schema validator and should not be presented as one. If your schema grows to nested objects, unions, or reusable definitions, use an appropriate validation library and test its configuration.

The evidence check adds a useful application rule beyond the schema: the returned passage must actually occur in the input. It still does not prove that the passage supports the selected status. A model could quote a real sentence and attach the wrong label. For a consequential action, keep a review step or implement a task-specific consistency check against a trusted system.

That limit matters in a home lab. A status extraction helper can organize notes for a dashboard. It should not turn a sentence from an arbitrary document into authority to shut down a machine. Keep reading data, interpreting data, and executing operations as separate application stages.

Can I test the failure paths without a GPU?

Yes. A validator should reject bad fixtures before you spend time tuning a model. Paste this block after validate_record and run it with your Python interpreter:

note = "office-pc is offline. The monitor did not receive a heartbeat."
good = {
    "device": "office-pc",
    "status": "offline",
    "evidence": "office-pc is offline."
}
assert validate_record(good, note) == good
bad_records = [
    {"device": "office-pc"},
    {**good, "status": "probably offline"},
    {**good, "status": 0},
    {**good, "extra": "unexpected"},
    {**good, "evidence": "The machine rebooted."}
]
for item in bad_records:
    try:
        validate_record(item, note)
    except ValueError as exc:
        print("REJECT:", exc)
    else:
        raise AssertionError("bad record was accepted")
print("PASS: valid fixture accepted; 5 invalid fixtures rejected")

The final line is the expected result and was reproduced locally. This establishes that the supplied rejection logic works for these fixtures. It says nothing about how often your chosen model will produce an acceptable answer. Measure that separately with representative notes, including ambiguous entries and missing information.

Keep the two test sets separate. Application tests should be repeatable and should not need inference. Model evaluation should preserve the input, model tag, request settings, and validation outcome. When a model changes, you can rerun the evaluation without rewriting the contract just to make the new answers pass.

Honest take: what should I change when generation still fails?

Change the layer that failed. The Ollama error reference describes HTTP error codes and the error property. A missing model or malformed request needs a request fix. It does not justify weakening the device-record validator. Preserve enough diagnostic information to see the actual failure, while avoiding private notes in shared logs.

For a schema rejection, inspect the raw generated content and the exact failing rule. If the application genuinely needs an optional value, change the contract deliberately and update its tests. Do not silently fill a missing status with online; that transforms missing evidence into a positive claim.

For incomplete generated JSON, check completion and generation limits before adding string-repair tricks. Appending a closing brace might make a document parse, but it cannot restore missing meaning. Keep the original failed answer for diagnosis and rerun only after identifying a reasonable correction.

Use bounded retries. In this example, a rejected record goes to review; automatic retries are not required. If you add a retry, count it, preserve the first failure, and impose a stop condition. Otherwise an extraction loop can keep the local server busy without producing useful records. Our Ollama queue and concurrency guide addresses overload separately.

If a request fails because the model cannot fit, investigate capacity with the VRAM calculator. Buying more memory does not fix a wrong response-field lookup or a missing required entry. Establish which problem you have before changing hardware.

FAQ

Q: Does format: "json" guarantee my required keys? A: Use a schema to express the fields you need, then validate the result in your application. JSON syntax alone does not define your record contract.

Q: Does temperature zero make extracted facts correct? A: No. Lower temperature is a generation setting, not a truth check. A structurally acceptable answer can still misread the input.

Q: Should I strip Markdown fences before parsing? A: First verify that the schema request reached the intended local endpoint. Treat unexpected prose or fences as a diagnostic failure; avoid a broad cleanup step that hides which component broke the contract.

Sources

Last updated November 4, 2026. Fixture tests are local Python checks; no model accuracy or hardware performance claim is implied.

Was this article helpful?

Get the numbers before you buy

New GPU and mini-PC benchmarks, VRAM thresholds, and price checks — sent when there's something worth acting on, not on a schedule. No spam, unsubscribe anytime.