Structured output

When the downstream consumer is code (a database write, a ticket, the next workflow node), “JSON-shaped prose” is not enough. Structured output upgrades the format block into a schema: missing fields, extra fields, and wrong types should fail at the API, not silently in business logic.

You are still writing a prompt. The schema is a machine-checkable output format. The prompt owns semantics (do not invent owner). The schema owns shape (priority is an enum).


JSON is shape; schema is the contract

Asking “return JSON only” in prose often fails:

  • A ```json fence or “Sure, here you go” wrapper
  • Fields appear and disappear
  • Numbers as strings
  • Hallucinated keys that were never in the source

A more stable stack:

LayerJob
PromptSemantic rules, refusal, field meaning
JSON Schema / PydanticRequired, types, enums, nesting
Vendor structured outputConstrained decoding, fewer illegal JSON-lookalikes
Your validatorsBusiness invariants (due cannot be in the past)

In LangChain 1.0 this is response_format=YourModel or with_structured_output (see Models and messages). In Dify, a parse / structured-output node sits after generation.


Write a human-readable schema first

{
  "title": "string, one-line title, no trailing period",
  "priority": "low | medium | high",
  "owner": "string | null if the text names no owner",
  "due": "YYYY-MM-DD | null"
}

Keep this in the prompt and in the code model. If they drift, code wins, then you fix the prompt—or Evaluation stays red forever.


Tiny Pydantic + OpenAI example

This uses Responses API parse (Chat Completions parse is the same idea: hand a Pydantic class to the SDK). Use a model your account actually has.

from pydantic import BaseModel, Field
from openai import OpenAI

class Ticket(BaseModel):
    title: str
    priority: str = Field(description="low, medium, or high")
    owner: str | None = None

client = OpenAI()
resp = client.responses.parse(
    model="gpt-4.1-mini",
    instructions="Extract a ticket. Do not invent owner. Set priority from urgency words only.",
    input="Login fails after reset. Please have the platform team look today.",
    text_format=Ticket,
)
ticket = resp.output_parsed
print(ticket.model_dump())

The prompt still matters: text_format does not teach “never invent owner.” The schema guarantees owner is a string or null; it does not guarantee you used null correctly.

On parse failure, retry or go to a human. Do not regex-rescue half the fields from prose—that destroys your eval baseline.


How the prompt should work with the schema

Extract a Ticket from the user message.
- Set owner only if a person or named team appears
- Do not emit keys outside the schema
- Do not speak outside the JSON

When you have a structured-output channel, do not also ask for “a detailed Markdown analysis and JSON in the same message.” One contract per call. If you need a chain of thought, use the vendor’s reasoning channel or split into two steps (prose analysis, then structure).


Misuse

  • JSON as a prose bucket (one giant notes field)—you did not structure anything
  • Tiny enums with 40 priorities—the model thrashs; start coarse
  • Extracting secrets or ID numbers because you can—see Pitfalls

Next steps

评论