Ollama JSON: Empty Results Are Not Failed Requests
An empty Ollama result can mean no matches or a broken request. I test the response parser so local extraction failures cannot pass as clean results.
I want my local extractor to say when it failed. Returning an empty list after a broken request hides the one fact I need.
An empty result is valid only after the request finishes and the response passes the application's checks. A timeout, invalid JSON, or missing field must remain a failure. JSON syntax alone cannot tell you whether the model found every item in the source.
Canonical URL: https://bmdpat.com/blog/ollama-json-empty-result-vs-failed-request-2026
Where does this affect my local stack?
My local model routing post describes separate prose and structured-data jobs. This post tests a smaller part of that system: the code that accepts an extraction result before another task uses it.
On September 13, 2026, I tested the Python function below with fixed response objects. These are test fixtures, not captured model outputs or a model accuracy benchmark. They let me check the caller's behavior without loading a model, changing a quant, or spending another inference request.
The target job is deliberately small. Given a note, extract a list of names. A note with no names can produce an empty list. A broken response cannot justify the same result.

What does Ollama return around the JSON?
For this example I use the native /api/generate response shape. Ollama documents response as the generated text and done as the completion flag. Its format field accepts JSON mode or a JSON schema. The endpoint streams by default; this example assumes the caller requests stream: false.
There are two objects to check. The outer object comes from the API. The inner JSON text comes from the model. Parsing the HTTP body does not also parse the text inside response.
I would pass an object schema with a required names array whose items are strings. Ollama's structured output guide shows how to request a schema and then validate the returned text. I still need application checks before the list reaches a downstream task.
How do I keep an empty result separate from failure?
Here is the complete acceptance function for the small example. It expects the decoded API object after the caller checks HTTP status. Transport errors must propagate before this function runs.
import json def accept_names(reply): if not isinstance(reply, dict): raise ValueError("API response must be an object") if "error" in reply or reply.get("done") is not True: raise ValueError("Generation failed or is incomplete") raw = reply.get("response") if not isinstance(raw, str): raise ValueError("Missing generated text") result = json.loads(raw) if not isinstance(result, dict) or set(result) != {"names"}: raise ValueError("Expected only the names field") names = result["names"] if not isinstance(names, list): raise ValueError("Names must be a list") if any(not isinstance(name, str) or not name.strip() for name in names): raise ValueError("Each name must be non-empty text") return names
Python's json.loads raises JSONDecodeError for invalid JSON. I let that exception reach the caller. I do not catch it and return [].
An empty list passes this function when the response contains the exact expected object. A missing list fails. A string in place of a list fails. A blank name fails. These are different inputs with different outcomes, even when the dashboard would otherwise show the same empty table.
Which cases did I test?
The passing fixtures contain an empty names list and a names list with Ada. The failing fixtures cover an API error, an incomplete response, absent generated text, malformed JSON, a missing field, the wrong field type, a blank name, and an unexpected extra field.
I also tested a decoded API value that is a list instead of an object, and a generated JSON value that is a list. Both fail. That matters because valid JSON includes values other than the object my application expects.
The fixture checks establish parser behavior. They do not establish that any local model will extract names correctly. Keep those results in separate test reports so a passing unit test cannot become an invented model score.
What can this check still miss?
A completed response can contain valid JSON and still omit Ada from a note that names her. This function would accept that empty list. To test extraction quality, use source fixtures with known expected names and compare the returned list against them.
Completion also deserves its own check. Keep the recorded stop reason with the response, as I explain in my stop-reason post. The function above checks the completion flag; it does not prove the model finished the intended task.
My practical rule is to preserve the failure at the boundary. Save the error category and enough redacted context to reproduce it. Let the caller choose a bounded retry or a visible failure. A blank result should mean the extraction returned no names, with its accuracy still subject to the task test.
Accompanying prompt
What the prompt does: Adds separate tests for valid empty output, request failure, and extraction accuracy in a local Ollama client.
Copy/paste this prompt:
Copy-ready prompt
Paste the exact block into your coding agent.
No article chrome, no footnotes, no formatting drift.
This prompt and every other one we publish live in the free prompt library.
Copy the block above.
I publish local AI tests and failure reports in The 5090 Reports. Join the email list for the next report.
Get the Local AI Field Kit
Four copy-ready tools now, then one evidence-backed Local AI Lab Note on Friday when there is something worth sharing.
Try the free agent run check firstGet the requested artifact now, then at most one evidence-backed Local AI Lab Note on Friday when there is something worth sharing. One-click unsubscribe. No sponsored placements. Privacy.
Patrick Hughes
I build BMD and publish measured AI runs, failure reports, and reusable checks. Nashville, Tennessee.
More writing
- 6 min
A kill rule that expires is not a kill rule
I wrote a kill rule for a paid-path test. Zero orders landed. The card expired into an archive and the dashboard still said ACTIVE.
- 5 min
Log Local LLM Fallbacks Before You Score the Output
My local writer hit a host RAM guard, then Sonnet produced a draft that failed QA. Here is how I separate availability, model identity, and useful output.
- 5 min
My local daily brief used 0 model calls
On September 4, 2026 my llama3.1:8b daily brief was ready with 0 model calls and 0 seconds. Preview-only file ranking is not a model run.
- 5 min
Long Context Cost Me VRAM, Not Tokens a Second
A 94x bigger prompt cost llama3.1:8b only 9.7% of its generation rate. VRAM rose 656 MiB, but num_ctx doubled in the same step. Budget memory first.
- 5 min
One model logged 14.5 and 6,178 prompt tokens a second
gemma4:26b read 53 prompt tokens at 14.5 tok/s and 4,470 at 6,178.9 on the same RTX 5090 run set. Prompt speed is not one number.