ChatClient.CallResponseSpec.entity() mapping LLM JSON to a record (TicketTriage, with a real enum-constrained Priority field), a List<ActionItem>, and a Map<String,Object> -- every case driven by a hand-written ScriptedChatModel with no live LLM anywhere. Key findings, all confirmed by disassembling spring-ai-client-chat-2.0.1.jar and spring-ai-model-2.0.1.jar rather than trusting docs: - StructuredOutputValidationAdvisor lives in org.springframework.ai.chat.client.advisor, in the same spring-ai-client-chat artifact as ToolCallingAdvisor -- unlike the tool-calling module's Tool Search Advisor pieces, it needs no separate Maven Central artifact or version pin. - entity(Class, spec -> spec.validateSchema()) is sugar: DefaultCallResponseSpec.resolveAdvisorChain builds a real StructuredOutputValidationAdvisor from the same JSON schema BeanOutputConverter uses to parse the response, and pushes it onto the advisor chain for that one call. - The schema/format instructions are baked into the user message once, up front, by entity() itself, before the advisor chain runs at all. A validation retry's only contribution is one appended line: "Output JSON validation failed because of: <the real schema-validator error>" -- each retry re-augments the ORIGINAL request, not the previous attempt's, so corrections never stack. - Default maxRepeatAttempts is 3 (4 total attempts); default advisorOrder is 2147481647, near Ordered.LOWEST_PRECEDENCE. - Exhausting every retry does NOT throw -- adviseCall's loop just returns the last (still invalid) response to the caller. Plain entity() with no validation, by contrast, throws immediately on the same bad JSON, since BeanOutputConverter.convert() is a separate Jackson deserialization step with no retry loop of its own. Both behaviors are captured from real runs (output/02, output/06). - Spring AI 2.0's JSON stack is Jackson 3 (tools.jackson.databind), not classic com.fasterxml.jackson -- visible directly in every one of this advisor's constructor and field signatures. Companion module for "Structured Output in Spring AI 2.0: Records, JSON Schema and Self-Correcting Responses" on ankurm.com. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
structured-output
Companion code for Structured Output in Spring AI 2.0: Records, JSON Schema and Self-Correcting Responses, part of the Spring AI series on ankurm.com.
Every test drives the real ChatClient.entity() machinery and the real StructuredOutputValidationAdvisor against a hand-written ScriptedChatModel (see support/ScriptedChatModel.java) that queues pre-programmed responses instead of calling a live provider. No test in this module calls a real LLM.
Versions
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| Spring AI | 2.0.1 |
| Java | 25 (LTS) |
StructuredOutputValidationAdvisor lives in org.springframework.ai.chat.client.advisor, inside the same spring-ai-client-chat artifact as ToolCallingAdvisor -- unlike the Tool Search Advisor pieces in this series' tool-calling module, it needs no separate Maven Central artifact or version pin.
Quickstart
./scripts/run-all.sh
Runs the full test suite and regenerates every file under output/.
What's here
| File | What it shows |
|---|---|
domain/TicketTriage.java |
The record used for the single-record examples; its Priority enum field is what gives the generated JSON schema a real enum constraint |
domain/Priority.java |
A closed four-value enum |
domain/ActionItem.java |
The record used for the List<ActionItem> example |
EntityBindingRecordTest.java |
entity(Class) on valid JSON, and the exception it throws with zero retries on invalid JSON |
EntityBindingListTest.java |
entity(ParameterizedTypeReference<List<T>>) |
EntityBindingMapTest.java |
entity(ParameterizedTypeReference<Map<String, Object>>) |
ValidationAdvisorRetriesOnceThenSucceedsTest.java |
entity(Class, spec -> spec.validateSchema()): one bad response, one corrective retry, one successful mapping |
ValidationAdvisorExhaustsRetriesTest.java |
StructuredOutputValidationAdvisor wired directly with maxRepeatAttempts(2); every attempt fails and the advisor returns the last invalid response without throwing |
Output files
| File | Captured from |
|---|---|
output/01-entity-record-valid.txt |
EntityBindingRecordTest.mapsValidJsonToARecord |
output/02-entity-record-invalid-no-retry.txt |
EntityBindingRecordTest.plainEntityThrowsImmediatelyOnAnInvalidEnumValue_noRetry |
output/03-entity-list-of-records.txt |
EntityBindingListTest |
output/04-entity-map.txt |
EntityBindingMapTest |
output/05-validation-advisor-retry-then-success.txt |
ValidationAdvisorRetriesOnceThenSucceedsTest |
output/06-validation-advisor-exhausts-retries.txt |
ValidationAdvisorExhaustsRetriesTest |
Requirements
JDK 25, Maven. No API key needed -- application.yml supplies a placeholder that the autoconfigured OpenAiChatModel bean would use, but no test ever constructs that bean.