with_structured_output closes the gap: it instructs the model to emit JSON that matches a schema, then parses the reply into a typed Python value for you.
The return type follows what you pass in. Hand it a plain dict (a JSON schema) and you get a dict back, parsed by the Rust JsonParser with no validation. Hand it a pydantic model class and you get a validated model instance back, parsed and validated in one fused pydantic-core call.
Two modes, one call
with_structured_output lives on every ChatModel. It returns a StructuredOutputModel wrapper that carries the same surface as the underlying model: generate, agenerate, stream, astream, invoke, and ainvoke.
| operator:
The pydantic path requires the optional extra:
pip install "infy[pydantic]". The dict path has no such requirement, the JSON parser is part of the core.How the mode is chosen
The wrapper decides once, at construction, which mode to use. It checks whetherschema is a pydantic BaseModel subclass. If it is, that is the pydantic path. Anything else (a plain dict) is the JSON-schema path.
- pydantic path
- JSON-schema dict path
- Detected when
schemais aBaseModelsubclass. - The schema instruction is built from
schema.model_json_schema(). - The reply is parsed and validated with
model_validate_json, pydantic-core’s fused parse-and-validate step. - You get a validated model instance back.
- If the model wrapped its JSON in prose or code fences, the wrapper falls back to extracting the JSON with
JsonParserand then validating. If that also fails, it surfaces the original validation error, not the parser’s error.
The instruction is cached
When you callwith_structured_output, the wrapper serializes your schema into a system message once and stores it. It is not re-serialized on every call.
That system instruction tells the model to return valid JSON conforming to the schema, and only the JSON: no explanation, no markdown, no code fences. On each generate call the wrapper prepends this cached SystemMessage to your messages, then sends them to the underlying model.
Streaming behavior
stream and astream yield partial dicts as tokens arrive, regardless of which mode you chose. Each chunk is parsed with JsonParser in partial mode, so you see the object fill in progressively.
Tool binding passes through
StructuredOutputModel is a wrapper, so it forwards bind_tools to the underlying model and returns a new structured wrapper around the tool-bound model. Your schema and its cached parser carry through unchanged.
When to use each
Reach for pydantic
You want validation, type coercion, and a typed object. Field constraints, required fields, and clear errors matter. You have installed
infy[pydantic].Reach for a dict schema
You want the lightest path with zero extra dependencies. You are comfortable handling an unvalidated
dict, or you validate downstream yourself.Related
Chat models
The
ChatModel protocol that with_structured_output extends.Composition
Chain a structured model with other runnables using the
| operator.