How to return structured output in LlamaIndex

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:

  1. 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

  2. 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")
  3. 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.

  4. 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.

  5. 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))
  6. 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.