Application code often needs named fields rather than an answer that must be parsed from free-form prose. LlamaIndex can bind an LLM response to a Pydantic model so routing, extraction, and agent logic receive a validated Python object.
The as_structured_llm() wrapper attaches the model class to an LLM. A completion then exposes the validated object through response.raw, while response.text remains the serialized response text.
A deterministic custom LLM keeps the schema handoff test independent of a provider key. The same TicketSummary model and wrapper apply to a provider-backed LLM; only the LLM construction changes.
Steps to return structured output in LlamaIndex:
- Install llama-index-core and Pydantic in the active Python environment.
$ python -m pip install llama-index-core pydantic Collecting llama-index-core ##### snipped ##### Successfully installed llama-index-core-0.14.23 ##### snipped #####
A provider-backed LLM also requires its integration package in the same environment.
Related: How to install LlamaIndex with pip - Create structured_output.py with the imports and ticket schema.
- structured_output.py
import json from collections.abc import Generator from typing import Any from llama_index.core.llms import CompletionResponse, CustomLLM, LLMMetadata from pydantic import BaseModel, Field class TicketSummary(BaseModel): team: str = Field(description="Team that should handle the ticket") priority: str = Field(description="Routing priority") next_action: str = Field(description="Next action for the team")
- Add the deterministic ticket-routing LLM below TicketSummary.
class TicketRouterLLM(CustomLLM): @property def metadata(self) -> LLMMetadata: return LLMMetadata(model_name="ticket-router", is_chat_model=False) def complete( self, prompt: str, formatted: bool = False, **kwargs: Any ) -> CompletionResponse: if "duplicate invoice" not in prompt.lower(): return CompletionResponse( text='{"team":"support","priority":"normal",' '"next_action":"review ticket"}' ) return CompletionResponse( text='{"team":"billing","priority":"high",' '"next_action":"review duplicate invoice"}' ) def stream_complete( self, prompt: str, formatted: bool = False, **kwargs: Any ) -> Generator[CompletionResponse, None, None]: yield self.complete(prompt, formatted=formatted, **kwargs)
The two prompt paths make the adapter deterministic while preserving an input-dependent result. A provider LLM replaces TicketRouterLLM in production.
- Add the structured completion call below TicketRouterLLM.
structured_llm = TicketRouterLLM().as_structured_llm( output_cls=TicketSummary ) response = structured_llm.complete( "Route this ticket: customer reports duplicate invoice INV-1042." ) ticket = response.raw
response.raw holds the validated TicketSummary instance. Invalid or missing fields cause Pydantic validation to fail instead of silently returning an incomplete object.
- Add type, field, and JSON checks below the completion call.
assert isinstance(ticket, TicketSummary) assert ticket.team == "billing" assert ticket.priority == "high" print(f"structured_type={type(ticket).__name__}") print(json.dumps(ticket.model_dump(), indent=2))
- Run the completed structured-output program.
$ python structured_output.py structured_type=TicketSummary { "team": "billing", "priority": "high", "next_action": "review duplicate invoice" }The assertions stop execution when response.raw is not the expected model or when the routed fields differ from the requested ticket.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.