← Back to list

Your LLM JSON Is Valid — And Still Wrong

You asked for JSON. The model gave you JSON. Your parser accepted it without complaint.

Rost Glukhov · 2026-05-26 14:34 · 6 claps · 4.4 min read
#technology #programming #artificial-intelligence #software-development #llm
Open on Medium ↗
Wiki topics: LLM · Large Language Models AI · AI · General 💻 · Programming

Your LLM JSON Is Valid — And Still Wrong

You asked for JSON. The model gave you JSON. Your parser accepted it without complaint.

Then your pipeline broke because the values were nonsense.

This is the quiet failure mode of LLM structured output. The shape is correct, the types check out, and the business logic is completely wrong. Schema validation alone cannot catch it. Most tutorials won’t tell you this because they stop at “happy path.”

Why This Matters

In production, a schema-valid payload with wrong values is worse than a parse error. A parse error fails loudly and immediately. A valid-but-wrong value fails downstream, hours later, in a place nobody expected. Support tickets get classified as the wrong category. Pricing data gets inverted. Tool arguments look correct but trigger the wrong handler.

The gap between “valid JSON” and “correct data” is where production incidents are born.

TL;DR

  • JSON mode and structured output enforce shape, not correctness — a response can match every field and still be wrong
  • Pydantic is the right validation layer in Python, but you need business-rule validators on top of it
  • Cross-field validation (like checking that a discounted price is actually lower) is something schemas alone cannot express
  • Retry with real validation errors, not generic “try again” prompts — cap attempts and fail closed
  • Test the contract with adversarial fixtures, not just golden examples

Symptoms

You’ve probably seen at least one of these:

  • The model returns JSON that parses fine, but contains a value your code doesn’t understand — like inventing a category that isn’t in your enum
  • A tool call fires with arguments that are valid JSON but have the wrong shape for the handler
  • Markdown fences or conversational preamble wrap around the payload, breaking the parser on the first non-happy-path input
  • Streaming responses get validated mid-stream because nobody buffered until finish_reason
  • A response passes schema checks but has logically impossible data — like discounted: true with no original price

These aren’t edge cases. They’re the normal failure surface of LLM structured output.

Root Cause

The root cause is treating structured output as a prompt problem instead of a contract problem.

When you ask a model to “return JSON,” you’re treating the output like a text generation task. When you define a schema, validate against it, and reject anything that doesn’t match, you’re treating it like an API boundary. The difference determines whether your system degrades gracefully or silently corrupts data.

OpenAI’s own documentation makes this distinction explicit. JSON mode gives you valid JSON. Structured Outputs enforces schema adherence. And even Structured Outputs, as OpenAI documents, does not prevent mistakes inside the values of the JSON object. A schema-valid response can still contain incorrect values.

The broader picture of how these constraints interact with throughput, retries, and runtime behavior is covered on the LLM performance engineering hub, which organizes the practical constraints that shape real-world LLM systems.

The Fix

The fix has four layers, and each one is non-negotiable in production:

Layer 1: Provider-side schema enforcement. Use Structured Outputs or strict tool schemas when the provider supports it. This shrinks the failure surface significantly. It does not eliminate it.

Layer 2: Python-side validation with Pydantic. Use model_validate_json() — not json.loads() followed by separate validation. The single-call path is faster and avoids double parsing. Close the object shape with extra="forbid" so the model can't sneak in extra fields.

Layer 3: Business-rule validation. This is the layer most tutorials skip. Use Pydantic’s model_validator for cross-field checks that a schema cannot express.

Here’s what that looks like in practice:

from decimal import Decimal
from pydantic import BaseModel, Field, model_validator

class Offer(BaseModel):
    currency: str
    amount: Decimal = Field(gt=0)
    original_amount: Decimal | None
    discounted: bool

    @model_validator(mode="after")
    def check_discount_logic(self):
        if self.discounted:
            if self.original_amount is None:
                raise ValueError("original_amount required when discounted")
            if self.original_amount <= self.amount:
                raise ValueError("original_amount must exceed amount")
        return self

A schema can say amount must be a positive decimal. It cannot say "if discounted is true, the original amount must be greater than the current amount." That logic lives in your business rules, not your schema.

Layer 4: Retry with real errors. When validation fails, send the specific validation error back to the model and ask for corrected output. Cap the attempts. Log the failure. Fail closed.

for attempt in range(2):
    completion = client.chat.completions.create(
        model="gpt-4o", messages=messages,
        response_format={"type": "json_object"}
    )
    try:
        ticket = TicketClassification.model_validate_json(raw_text)
        break
    except ValidationError as exc:
        messages.append({
            "role": "user",
            "content": f"Validation failed with {exc.errors()}. Return corrected JSON only."
        })
else:
    raise RuntimeError("exhausted structured output retries")

The key detail: the retry is driven by real validator output, not a generic “try again” message. The model needs to know what was wrong.

Verification

How do you know your validation pipeline is actually working? Test it like an API contract, not like a prompt.

Golden fixtures. Use jsonschema.validate() to check that your test payloads match the schema. This catches field additions or removals before they hit the model.

Adversarial cases. Write tests for the things that break:

# Rejects a category not in the enum
with pytest.raises(ValidationError):
    TicketClassification.model_validate({"category": "refund", ...})

# Rejects extra keys the model invented
with pytest.raises(ValidationError):
    TicketClassification.model_validate({...extra_field: "surprise"})

# Rejects logically impossible cross-field data
with pytest.raises(ValidationError):
    Offer.model_validate({
        "currency": "USD", "amount": "19.00",
        "original_amount": "10.00", "discounted": True
    })

Handle the documented failure modes. Refusals and incomplete responses bypass your schema entirely. Check for them explicitly:

if response.status == "incomplete":
    raise RuntimeError(response.incomplete_details.reason)

if content.type == "refusal":
    raise RuntimeError(content.refusal)

These aren’t defensive over-engineering. They’re directly aligned with documented provider behavior.

Quick Fix

  • Use provider-side Structured Outputs when available, not JSON mode
  • Validate in Python with Pydantic’s model_validate_json(), not json.loads() + separate validation
  • Add model_validator for cross-field business rules that schemas cannot express
  • Retry with specific validation errors, capped at 2–3 attempts
  • Fail closed on refusal and incomplete response — never silently accept them
  • Test with adversarial fixtures, not just golden examples
  • Use extra="forbid" to prevent the model from inventing fields

Takeaway

Structured output validation is not a prompt engineering problem. It’s a contract problem. Define the shape, enforce it at the provider level when possible, mirror it in Python with Pydantic, add business rules for what the schema cannot prove, and handle the failure modes that every provider documents but few applications check.

The teams that get this right don’t have fewer bugs because they use better models. They have fewer bugs because they treat LLM output like an untrusted API — and build validation pipelines that would make a traditional web service jealous.

👉 LLM Structured Output Validation in Python That Holds Up


메타데이터
post_id
12dbbecf1fdc
slug
your-llm-json-is-valid-and-still-wrong-12dbbecf1fdc
url
https://medium.com/@rosgluk/your-llm-json-is-valid-and-still-wrong-12dbbecf1fdc
canonical_url
https://medium.com/@rosgluk/your-llm-json-is-valid-and-still-wrong-12dbbecf1fdc
author_url
https://medium.com/@rosgluk
status
ok
fetched_at
2026-06-15 20:49:13