Structured Output
The problem
You ask the model for a list of movies. It gives you a nice paragraph. Great for humans, terrible for code. You need a List<String> or a POJO, not prose.
Structured output solves this — tell the model what format to return, parse the response into your target type.
Level 1: .entity() (the easy way)
ChatClient handles everything with .entity():
Transcript example
Say you have a /structured endpoint that takes a transcript and returns structured data:
@Data
public class TranscriptModel {
private String conversationId;
private String problem;
private String sentiment;
private boolean resolution;
}
TranscriptModel result = chatClient.prompt()
.user(u -> u.text("Given this transcript, convert it into a structured format. {transcript}")
.param("transcript", transcript))
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.entity(new ParameterizedTypeReference<TranscriptModel>() {});
Clean — your API returns typed data directly, not a random text blob.
Same idea with a simpler example:
record ActorFilms(String actor, List
ActorFilms result = ChatClient.create(chatModel).prompt() .user("Generate the filmography of 5 movies for Tom Hanks.") .call() .entity(ActorFilms.class);
// result.actor() → "Tom Hanks" // result.movies() → ["Forrest Gump", "Cast Away", ...]
That's it. Spring AI generates a JSON schema from your record, tells the model to return JSON matching it, deserializes with Jackson. All behind the scenes.
### Collections
Need a list of objects? Use `ParameterizedTypeReference`:
```java
List<ActorFilms> results = ChatClient.create(chatModel).prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
Level 2: Maps and lists
Sometimes you don't need a full POJO.
Map output
Map<String, Object> result = ChatClient.create(chatModel).prompt()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under the key name 'numbers'"))
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
List output
List<String> flavors = ChatClient.create(chatModel).prompt()
.user(u -> u.text("List five {subject}")
.param("subject", "ice cream flavors"))
.call()
.entity(new ListOutputConverter(new DefaultConversionService()));
Level 3: Native structured output
Here's where it gets interesting. Modern models (GPT-4o, Claude 3.5, Gemini 1.5 Pro) support structured output natively — the model guarantees valid JSON matching your schema. No prompt-based formatting needed.
ActorFilms result = ChatClient.create(chatModel).prompt()
.advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
Or set it globally:
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
.build();
}
Why native over prompt-based?
- More reliable — the model guarantees schema compliance
- Cleaner prompts — no format instructions cluttering things
- Better performance — models optimize for this internally
Models that support it:
- OpenAI: GPT-4o and later
- Anthropic: Claude 3.5 Sonnet and later
- Vertex AI: Gemini 1.5 Pro and later
- Mistral AI: Mistral Small and later
Level 4: Low-level BeanOutputConverter
If you're working at the ChatModel level (not ChatClient), use converters directly:
BeanOutputConverter<ActorFilms> converter = new BeanOutputConverter<>(ActorFilms.class);
String format = converter.getFormat();
String template = """
Generate the filmography of 5 movies for {actor}.
{format}
""";
Generation generation = chatModel.call(
PromptTemplate.builder()
.template(template)
.variables(Map.of("actor", "Tom Hanks", "format", format))
.build()
.create()
).getResult();
ActorFilms result = converter.convert(generation.getOutput().getText());
More verbose, but useful when you need full control.
Property ordering
Control the order of properties in the generated JSON schema with @JsonPropertyOrder:
@JsonPropertyOrder({"actor", "movies"})
record ActorFilms(String actor, List<String> movies) {}
Getting entity AND metadata
Sometimes you need the structured entity and model metadata (token usage, etc.):
ResponseEntity<ActorFilms> responseEntity = ChatClient.create(chatModel).prompt()
.user("Generate the filmography for a random actor.")
.call()
.responseEntity(ActorFilms.class);
ActorFilms films = responseEntity.getEntity();
ChatResponse chatResponse = responseEntity.getChatResponse();
// chatResponse has token counts, model info, etc.
Things to remember
- Use
.entity()on ChatClient — simplest path from LLM output to Java objects - The TranscriptModel pattern works well for multi-field responses
- Try native structured output first if your model supports it
- Records and classes both work — pick whatever fits your team
- Validate at API boundaries — this is best-effort from the model
- Don't use structured output for tool calling — tool calling already gives you structured params