How to get structured output from AI with Java records in Spring AI
Purpose
When I was building my first AI app with Spring AI, I ran into a problem. The AI returned responses as plain text, and I had to manually parse JSON strings into Java objects. This was error-prone and tedious.
I wanted a way to get structured, type-safe objects directly from AI responses without writing JSON parsing code.
In this post, I’ll show how I used Java records with Spring AI’s .entity() method to automatically convert AI responses into Java objects.
The Problem: Manual JSON Parsing
When I started with Spring AI, I called the AI like this:
String response = chatClient.prompt() .user("Generate filmography for Tom Hanks") .call() .getContent();
// I got back a JSON string like:// {"actor": "Tom Hanks", "movies": ["Forrest Gump", "Cast Away", ...]}Then I had to manually parse it:
ObjectMapper mapper = new ObjectMapper();JsonNode jsonNode = mapper.readTree(response);
String actor = jsonNode.get("actor").asText();List <String> movies = new ArrayList <>();jsonNode.get("movies").forEach(node -> movies.add(node.asText()));
ActorFilms result = new ActorFilms(actor, movies);This approach had several issues:
- Verbose: Too much boilerplate code for simple data extraction
- Error-prone: If the AI returned malformed JSON, the parsing failed at runtime
- No type safety: I couldn’t catch mismatches until the code ran
- Fragile: If the AI changed its response format, I had to update the parsing logic
I wanted something better.
The Solution: Java Records + Structured Output
I discovered that Spring AI supports structured output through the .entity() method. Here’s how I use it.
First, I define a Java record to represent the structure I want:
public record ActorFilms(String actor, List <String> movies) {}Then I use the .entity() method to automatically convert the AI response:
ActorFilms actorFilms = chatClient.prompt() .user("Generate filmography for Tom Hanks") .call() .entity(ActorFilms.class);
// That's it! I get a fully-populated ActorFilms objectSystem.out.println(actorFilms.actor()); // "Tom Hanks"System.out.println(actorFilms.movies()); // ["Forrest Gump", "Cast Away", ...]No manual JSON parsing. Spring AI handles everything automatically.
How It Works
When I call .entity(ActorFilms.class), Spring AI does several things behind the scenes:
- Inspect the record: It reads the field names and types from the
ActorFilmsrecord - Generate instructions: It tells the AI to return JSON matching that structure
- Parse the response: When the AI responds, it automatically converts the JSON to the record type
- Validate: If the response doesn’t match the expected structure, it throws an exception
This means I get compile-time type checking. If I misspell a field name or use the wrong type, my IDE catches it immediately.
Handling Multiple Records
I also needed to get lists of structured objects. For example, generating filmography for multiple actors:
List <ActorFilms> actorFilms = chatClient.prompt() .user("Generate filmography for Tom Hanks and Bill Murray") .call() .entity(new ParameterizedTypeReference<List <ActorFilms>>() {});
actorFilms.forEach(af -> { System.out.println(af.actor() + ": " + af.movies());});The ParameterizedTypeReference lets Spring AI know I want a list of ActorFilms objects rather than a single object.
Using Native Structured Output
I found that some AI models support native structured output (like OpenAI’s structured output feature). When available, this provides better performance and reliability.
Here’s how I enable it:
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
ActorFilms actorFilms = ChatClient.create(chatModel) .prompt() .advisors(new QuestionAnswerAdvisor(chatMemory)) // Enable native support .user("Generate the filmography of 5 movies for Tom Hanks") .call() .entity(ActorFilms.class);When native structured output is available, the AI model guarantees the response matches the expected structure. This eliminates validation errors.
Why Records Instead of Classes?
I use Java records instead of regular classes for structured output. Here’s why:
- Immutability: Records are immutable by default, which is perfect for data transfer objects
- Concise: One line of code vs. multiple lines for a class with constructor, getters, equals, hashCode
- Better semantics: Records are designed to hold data, not behavior
- Automatic pattern matching: Records work better with Java’s pattern matching features
Here’s a comparison:
// Record - clean and simplepublic record ActorFilms(String actor, List <String> movies) {}
// Class - verbose boilerplatepublic class ActorFilms { private final String actor; private final List <String> movies;
public ActorFilms(String actor, List <String> movies) { this.actor = actor; this.movies = movies; }
public String getActor() { return actor; } public List <String> getMovies() { return movies; }
@Override public boolean equals(Object o) { /* ... */ } @Override public int hashCode() { /* ... */ }}The record version is much clearer and focuses on the data structure itself.
Common Mistakes I Made
When I first started using structured output, I made some mistakes:
Mistake 1: Not using records
I tried using regular classes first. This worked, but I had to write getters and ensure proper constructors. Switching to records simplified everything.
Mistake 2: Manual JSON parsing
I initially tried to parse JSON manually using ObjectMapper even though structured output was available. This was unnecessary work.
Mistake 3: Ignoring native structured output
I didn’t realize that some AI models have native support for structured output. Enabling it improved reliability and performance.
Summary
In this post, I showed how to get structured output from AI using Java records in Spring AI. The key points are:
- Use
.entity()onChatClientto automatically convert AI responses to Java objects - Define records to represent your expected data structure
- Use
ParameterizedTypeReferencefor lists of objects - Enable native structured output when available for better reliability
This approach eliminates manual JSON parsing, provides compile-time type safety, and makes AI integration in Java applications much cleaner.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring AI Structured Output Documentation
- 👨💻 Built my first AI app entirely in Java using Spring AI
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments