Files
Claude 7fff4ddb99 Add structured-output module: entity() mapping to records/lists/maps, StructuredOutputValidationAdvisor retries
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
2026-09-23 17:18:32 +00:00
..

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.