Skip to content

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:

ManualJSONParsing.java
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:

ManualJSONParsing.java
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:

ActorFilms.java
public record ActorFilms(String actor, List <String> movies) {}

Then I use the .entity() method to automatically convert the AI response:

StructuredOutputExample.java
ActorFilms actorFilms = chatClient.prompt()
.user("Generate filmography for Tom Hanks")
.call()
.entity(ActorFilms.class);
// That's it! I get a fully-populated ActorFilms object
System.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:

  1. Inspect the record: It reads the field names and types from the ActorFilms record
  2. Generate instructions: It tells the AI to return JSON matching that structure
  3. Parse the response: When the AI responds, it automatically converts the JSON to the record type
  4. 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:

MultipleRecordsExample.java
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:

NativeStructuredOutputExample.java
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:

RecordVsClass.java
// Record - clean and simple
public record ActorFilms(String actor, List <String> movies) {}
// Class - verbose boilerplate
public 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() on ChatClient to automatically convert AI responses to Java objects
  • Define records to represent your expected data structure
  • Use ParameterizedTypeReference for 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments