Learn how to use Spring AI Structured Output converters to transform AI-generated text into Java objects, lists, and maps with practical examples.
1. Introduction
When working with AI models, one of the biggest challenges is handling their responses. AI models typically output raw text, which needs to be transformed into structured data that your application can use. Spring AI’s Structured Output Converter solves this problem elegantly, helping you convert AI-generated text into usable Java objects, lists, or maps.
In this guide, we’ll explore how Spring AI makes it easy to work with AI responses in a structured way, with real-world examples that demonstrate each approach.
2. Understanding Spring AI Structured Output
Before diving into code examples, let’s understand what Spring AI Structured Output Converter does. Imagine you’re having a conversation with an AI assistant. The AI responds in natural language, but your application needs specific data points from that response, not just text.
The Structured Output Converter acts as a translator between the AI’s natural language responses and the structured data formats your application needs. It works in two key ways:
- Before the AI call: It adds formatting instructions to your prompt, essentially telling the AI model how to structure its response.
- After the AI call: It converts the AI’s response into your desired data structure (Java objects, lists, maps, etc.).
This means you can focus on what you want to ask the AI, while Spring AI handles the complexities of extracting usable data from the response.
3. Meet the Converters: Your Data Shaping Tools
At the heart of Spring AI Structured Output are specialized tools called Structured Output Converters. Think of them as expert translators who know exactly how to ask the AI for data in a specific format and then convert the AI’s text response into that format for your Java application.
Here are the main types of converters:
- BeanOutputConverter<T>: This is your go-to when you want the AI’s output to directly become an instance of your custom Java class or record. It cleverly figures out the structure of your Java class and tells the AI to provide data in a matching JSON format, which it then turns into your Java object.
- MapOutputConverter: Use this when you need a flexible key-value structure, like a standard Java Map<String, Object>. It instructs the AI to produce a JSON object, which then gets converted into a map. This is handy when the output structure isn’t rigidly defined by a specific Java class.
- ListOutputConverter: If you simply need a list of items, especially a list of strings, this converter is perfect. It typically asks the AI for a comma-separated list and then turns that into a Java List.
🎉 The Good News for Fluent API Users:
When you use Spring AI’s modern ChatClient fluent API (which we’ll be using in our examples!), you often don’t have to create these converters directly. When you call methods like .call().entity(YourClass.class)
or .call().entity(new ParameterizedTypeReference<List<YourClass>>(){})
, Spring AI intelligently selects and uses the appropriate converter (like BeanOutputConverter or ListOutputConverter) behind the scenes. It makes getting structured data incredibly straightforward!
Now that we know a bit about these helpful converters, let’s see them in action by building our Social Media App!
4. Our Example Application: Social Media Analytics
Throughout this tutorial, we’ll build a simple social media analytics application that uses AI to analyze and process social media data. We’ll create endpoints that:
- Generate a JSON response with basic user engagement metrics
- Convert AI responses into specific entity types (like user profiles)
- Generate lists of entities (like trending topics)
- Create map-based outputs (like sentiment analysis by platform)
Let’s start building our application step by step.
⚙️ Setting Up Spring AI Project
Let’s set up our project with the necessary dependencies and configurations.
Step 1: Add Maven Dependencies
Add these dependencies to pom.xml
file:
<dependencies>
<!-- Spring Boot Web for building RESTful web services -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenAI Model Support – configureable for various AI providers (e.g. OpenAI, Google Gemini) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- Spring AI bill of materials to align all spring-ai versions -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
pom.xmlIn this configuration:
spring-boot-starter-web:
Enables us to build a web application with REST endpointsspring-ai-starter-model-openai:
Provides integration with OpenAI’s API (though we’ll configure it for Google Gemini)spring-ai-bom:
ThedependencyManagement
section uses Spring AI’s Bill of Materials (BOM) to ensure compatibility between Spring AI components. By importing the BOM, you don’t need to manually specify versions for each Spring AI artifact—it ensures compatibility and prevents version conflicts automatically.
Step 2: Configure Application Properties
Now, let’s configure our application and add configuration related to AI using application.yml
:
spring:
application:
name: spring-ai-structured-output
# AI configurations
ai:
openai:
api-key: ${GEMINI_API_KEY}
base-url: https://generativelanguage.googleapis.com/v1beta/openai
chat:
completions-path: /chat/completions
options:
model: gemini-2.0-flash-exp
application.yaml📄 Configuration Overview
This configuration focuses on AI integration with Google’s Gemini model via the Spring AI OpenAI starter:
👉 AI (OpenAI Starter) Settings
- api‑key: Your secret key for authenticating with the AI service. Keep this safe and out of source control.
- base‑url: Overrides the default OpenAI endpoint so requests go to Google’s Gemini API instead.
- completions‑path: The REST path for chat-based completions—appended to the base URL when making requests.
- model: Chooses which AI model to call (e.g.
gemini-2.0-flash-exp
). This determines the capabilities and response style you’ll get back.
Make sure to set the GEMINI_API_KEY
environment variable with your actual Google Gemini API key before running the application.
🤖 Google Gemini APIs are great for proof-of-concept (POC) projects since they offer limited usage without requiring payment. For more details, check out our blog, where we dive into how Google Gemini works with OpenAI and how to configure it in case of our Spring AI application.
Step 3: Create Controller
Now, let’s create a REST controller to expose endpoints for generating different types of content:
import com.bootcamptoprod.dto.TrendingTopic;
import com.bootcamptoprod.dto.UserProfile;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RestController
@RequestMapping("/api/social")
public class SocialMediaAnalyticsController {
private final ChatClient chatClient;
public SocialMediaAnalyticsController(ChatClient.Builder chatClient) {
this.chatClient = chatClient.build();
}
// We'll add our endpoints here...
}
SocialMediaAnalyticsController.javaThe constructor injects a ChatClient.Builder and builds a reusable ChatClient instance for making AI chat calls throughout the controller. By using constructor injection for ChatClient, we ensure a single, ready-to-use client is available whenever you need to send prompts and receive structured AI responses.
This controller sets up the base structure for our API endpoints. We’ll add specific methods for generating different types of content using various Structured Output Converters.
Now, let’s explore each type of converter and how to use them.
5. Generating Simple JSON Responses with Built-in JSON Mode
Sometimes, you just want the AI to give you a raw JSON string. Some AI models, like those from OpenAI, have a “JSON mode” that ensures their output is valid JSON. You can enable this through application properties if you want all responses from that model configuration to attempt JSON.
Let’s enable it in application.properties for OpenAI:
spring:
ai:
openai:
chat:
options:
responseFormat:
type: json_object
application.yaml📌 Important: Setting responseFormat.type: JSON_OBJECT
globally like this means the model will always try to output JSON. If you only want JSON for specific calls, you’d configure ChatOptions per call or use converters like MapOutputConverter which ask for JSON in the prompt (covered later). For this example, we’ll assume the global setting.
Let’s add an endpoint for generating a sample social media engagement metrics in JSON format:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping(value = "/engagement", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<String> getEngagementMetrics(@RequestParam String platform) {
String userPrompt = "Generate sample social media engagement metrics for {platform} including likes, shares, comments, and reach for the last 7 days.";
String response = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("platform", platform))
.call()
.content();
return ResponseEntity.ok(response);
}
SocialMediaAnalyticsController.javaExplanation:
- We create a prompt asking for social media engagement metrics in a JSON format.
- Because
spring.ai.openai.chat.options.responseFormat.type=JSON_OBJECT
is set in application.properties, the LLM is instructed to output valid JSON. .call().content()
retrieves the AI’s response as a plain string. This string should be the JSON you requested.
🖥️ Verify the output
When you send a GET request tohttp://localhost:8080/api/social/engagement?platform=Instagram
the controller returns a raw JSON array—each element representing a day’s engagement metrics (date, likes, comments, shares, reach). This format is ideal for feeding into frontend dashboards, analytics pipelines, or any tool that can parse JSON. You’ll see something like:
📌 Important: Remember to remove or comment out spring.ai.openai.chat.options.responseFormat.type=JSON_OBJECT
from application.properties or application.yml for the subsequent examples, as they will use converters that manage their own format requests.
6. Converting AI Responses to Java Entities with BeanOutputConverter
The BeanOutputConverter
allows you to map AI responses directly to Java objects. This is perfect when you have a well-defined entity structure.
This is where Spring AI Structured Output truly shines for many Java developers. You often want the AI’s response to map directly to a Java class (like our UserProfile record). The BeanOutputConverter (used implicitly by ChatClient’s .entity() method when a class is provided) is perfect for this.
Let’s add an endpoint for generating a social media user profile:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping("/profile")
public ResponseEntity<UserProfile> getUserProfile(@RequestParam String username) {
String userPrompt = "Generate a sample social media profile for a user with the username {username}.";
UserProfile profile = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("username", username))
.call()
.entity(UserProfile.class);
return ResponseEntity.ok(profile);
}
// ... (other endpoints from previous examples) ...
SocialMediaAnalyticsController.javaExplanation:
- We define a user message asking the AI to generate a sample social media profile for provided username.
- The key part is
.call().entity(UserProfile.class)
. This tells Spring AI: “I expect the AI’s response to be convertible into a UserProfile object.” - Behind the scenes, Spring AI uses BeanOutputConverter. It analyzes your UserProfile record (including nested SocialMediaMetrics record), crafts instructions for the LLM to produce JSON matching that structure, and then converts the LLM’s JSON response into an actual UserProfile instance.
Here are the UserProfile and SocialMediaMetrics Java records for reference:
import java.util.List;
public record UserProfile(
String username,
String fullName,
int followers,
int following,
List<String> interests,
SocialMediaMetrics metrics
) {
}
UserProfile.javapublic record SocialMediaMetrics(
int likes,
int shares,
int comments,
int reach,
String engagementRate,
String impressions,
String followersGrowthRate
) {
}
SocialMediaMetrics.java🖥️ Verify the output
Now, if you access http://localhost:8080/api/social/profile?username=John
, you’ll get a nicely formatted JSON response from your Spring Boot app, which is actually a serialized UserProfile object!
7. Getting a List of Your Custom Java Objects
Often, you’ll need the AI to generate not just one, but multiple structured items. For example, you might want a list of TrendingTopic objects, where each object has its own set of fields like topic, popularity, and related hashtags.
For this scenario, you’ll use the versatile BeanOutputConverter (which ChatClient uses implicitly) along with ParameterizedTypeReference to tell Spring AI that you’re expecting a list of your custom Java objects.
Let’s add an endpoint to SocialMediaAnalyticsController for generating multiple trending topics related to a specified category.
First, ensure you have the TrendingTopic record defined:
import java.util.List;
public record TrendingTopic(
String topic,
int popularity,
List<String> relatedHashtags
) {
}
TrendingTopic.javaNow, the controller endpoint:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping("/trending")
public ResponseEntity<List<TrendingTopic>> getTrendingTopics(@RequestParam String category) {
String userPrompt = "Generate 5 trending topics on social media related to {category}";
List<TrendingTopic> topics = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("category", category))
.call()
.entity(new ParameterizedTypeReference<List<TrendingTopic>>() {
});
return ResponseEntity.ok(topics);
}
// ... (other endpoints from previous examples) ...
SocialMediaAnalyticsController.javaExplanation:
- The Prompt: We ask the AI to generate multiple trending topics, specifying the fields we expect for each (topic, popularity, relatedHashtags).
- ParameterizedTypeReference: The crucial part is
.call().entity(new ParameterizedTypeReference<List<TrendingTopic>>() {})
. This tells Spring AI: “I expect the AI’s response to be convertible into a List where each element is a TrendingTopic object.” - Behind the Scenes (BeanOutputConverter):
- Spring AI’s ChatClient uses the BeanOutputConverter here.
- The BeanOutputConverter analyzes your TrendingTopic record and the fact that you’ve requested a List.
- It then constructs format instructions for the LLM, guiding it to produce a JSON array. Each element in this array should be a JSON object that matches the structure of your TrendingTopic record.
- After the LLM responds with the JSON array, the BeanOutputConverter uses its underlying JSON deserialization mechanism (e.g., Jackson ObjectMapper) to convert that JSON array into a java.util.List<TrendingTopic>.
🖥️ Verify the output
Now, if you access http://localhost:8080/api/social/trending?category=AI
, you’ll get a JSON array of TrendingTopic objects.
8. Getting a Simple List with ListOutputConverter
What if you don’t need a list of complex objects, but rather a simple list of strings? For example, you might want the AI to suggest a list of content keywords based on the specified topic.
For this, Spring AI provides the ListOutputConverter. It’s designed for simpler list structures and typically expects the AI to return a comma-separated string of items.
Let’s add an endpoint for generating multiple content keywords based on the specified topic:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping("/suggest-keywords")
public ResponseEntity<List<String>> suggestContentKeywords(@RequestParam String topic) {
String userPrompt = String userPrompt = "Suggest 5 to 7 relevant content keywords or short phrases for social media posts about '{topic}'.";
List<String> keywords = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("topic", topic))
.call()
.entity(new ListOutputConverter(new DefaultConversionService())); // Using ListOutputConverter
return ResponseEntity.ok(keywords);
}
// ... (other endpoints from previous examples) ...
SocialMediaAnalyticsController.javaExplanation:
- The Prompt – Clear Instructions for the AI: We’ve crafted a specific prompt that asks the AI to “Suggest 5 to 7 relevant content keywords or short phrases”. This part of the prompt focuses on what content we want (keywords/phrases), the quantity (5 to 7), and the context (social media posts about a specific {topic}). While this user-defined prompt sets the stage for the content, Spring AI’s ListOutputConverter will later augment this with instructions about the format (like asking for a comma-separated list).
- ListOutputConverter – The Right Tool for Simple Lists: The core of this endpoint’s structured output capability is
.call().entity(new ListOutputConverter(new DefaultConversionService()))
. By explicitly using ListOutputConverter, we’re telling Spring AI that we expect a simple list, typically of strings. The DefaultConversionService is a Spring utility that helps the converter handle the individual items after the AI’s response is split; for a List<String>, this is a direct string-to-string step. - Behind the Scenes – From Text to List<String>:
- When this code executes, the ListOutputConverter subtly adds its own formatting instructions to the prompt sent to the LLM, reinforcing the request for a comma-delimited string.
- When the LLM responds (e.g., with a string like “AI in marketing, content personalization, chatbot marketing”), the ListOutputConverter takes over.
- It parses this single string, splitting it by the comma delimiter, and transforms it into a java.util.List<String> which is then returned by your controller.
🖥️ Verify the output
Now, if you access http://localhost:8080/api/social/suggest-keywords?topic=AI in marketing, content personalization, chatbot marketing
, you’ll get a list of string.
9. Generating Map-Based Output with MapOutputConverter
The MapOutputConverter
is useful when you need a flexible structure with key-value pairs. This is perfect for scenarios where the structure might vary or when you’re working with dynamic data.
Let’s add an endpoint to fetch a summary of a social media campaign, where the details might be diverse and best represented as a map:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping("/campaign-summary")
public ResponseEntity<Map<String, Object>> getCampaignSummary(@RequestParam String name) {
String userPrompt = """
Provide a summary for the social media campaign named '{name}' for different social media platforms (Twitter, Instagram, Facebook).
Include key metrics like:
- Total Impressions
- Click-Through Rate (CTR) as a percentage
- Total Budget Spent (in USD)
- A brief description of the Target Audience
- Overall Performance Assessment (e.g., "Exceeded Expectations", "Met Goals", "Needs Improvement")
""";
// Explicitly using MapOutputConverter
Map<String, Object> campaignSummary = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("name", name))
.call()
.entity(new MapOutputConverter()); // <-- Explicitly providing the converter
return ResponseEntity.ok(campaignSummary);
}
// ... (other endpoints from previous examples) ...
SocialMediaAnalyticsController.javaExplanation:
- The Prompt & Explicit Converter – Requesting a JSON Map: Our userPrompt requests a campaign summary with specific metrics. The line
.call().entity(new MapOutputConverter())
then directly instructs Spring AI to use this newly created MapOutputConverter instance to process the LLM’s response. This is ideal when you want to be certain this specific converter is used, potentially with instance-specific configurations if available. - MapOutputConverter in Action – Guiding the LLM and Parsing JSON: The MapOutputConverter automatically enhances the userPrompt by adding instructions for the LLM to format its entire output as a single, valid JSON object. Once the LLM returns the JSON string, the converter uses its internal JSON processing capabilities (typically Jackson) to parse this string.
- Result – A Flexible java.util.Map:
- The outcome of the parsing process is a java.util.Map<String, Object>.
- The keys of this map correspond to the keys from the JSON object generated by the LLM, and the values are the associated JSON values converted to their Java equivalents.
- This Map provides a versatile way to handle responses where the structure might be dynamic or not easily mappable to a static Java class.
🖥️ Verify the output
If you access http://localhost:8080/api/social/campaign-summary?name=AI
, the controller will use the MapOutputConverter to process the LLM’s response, and you should receive a JSON object in your browser or API client, representing the campaign summary as a map.
10. Leveraging Parameterized Type Reference for Flexible Map Outputs
When interacting with an LLM, you might need a flexible, dynamic data structure like a Java Map to capture the response, especially when the exact keys or nesting might vary. Spring AI facilitates this by allowing you to specify your desired output type using ParameterizedTypeReference.
Let’s see an example where we want a sentiment analysis for a brand, returned as a Map<String, Object>:
// ..... Existing SocialMediaAnalyticsController code
@GetMapping("/sentiment")
public ResponseEntity<Map<String, Object>> getSentimentAnalysis(@RequestParam String brand) {
String userPrompt = "Analyze the sentiment for the brand {brand} across different social media platforms (Twitter, Instagram, Facebook). Include sentiment scores from 0-100 and most common sentiments.";
Map<String, Object> sentiment = chatClient.prompt()
.user(input -> input.text(userPrompt)
.param("brand", brand))
.call()
.entity(new ParameterizedTypeReference<Map<String, Object>>() {});
return ResponseEntity.ok(sentiment);
}
// ... (other endpoints from previous examples) ...
SocialMediaAnalyticsController.javaExplanation:
- The Prompt – Requesting Key-Value Insights:
Our userPrompt (“Analyze the sentiment for the brand {brand}…”) asks for various pieces of information that naturally lend themselves to a key-value structure. This makes a Map an ideal target for the AI’s potentially complex and nested response. - Specifying the Desired Output as a Map with ParameterizedTypeReference:
- The core of this technique lies in the line:
.call().entity(new ParameterizedTypeReference<Map<String, Object>>() {})
- Here, new ParameterizedTypeReference<Map<String, Object>>() {} explicitly tells Spring AI that the expected Java type of the final, converted LLM response should be a Map<String, Object>.
- ParameterizedTypeReference is a special class used to capture and pass generic type information, which Java’s type erasure would otherwise lose at runtime. This ensures Spring AI knows precisely what kind of collection you’re expecting.
- The core of this technique lies in the line:
- Behind the Scenes – How the Map is Materialized (using BeanOutputConverter):
- The BeanOutputConverter analyzes the
ParameterizedTypeReference<Map<String, Object>>
you provided and the fact that you’ve requested a Map. - It then constructs format instructions for the LLM, guiding it to produce a JSON response. This JSON response should represent the key-value pairs expected for your map structure.
- After the LLM responds with the JSON object, the BeanOutputConverter uses its underlying JSON deserialization mechanism to convert that JSON response into a java.util.Map<String, Object>.
- The BeanOutputConverter analyzes the
🖥️ Verify the output
Now, if you access http://localhost:8080/api/social/sentiment?brand=TATA Trust
, you’ll get a Map.
11. Source Code
Want to dive into the code and run these examples yourself? You can find the complete working project demonstrating the Spring AI Structured Output techniques discussed in this blog post on our GitHub repository.
🔗 Spring AI Structured Output Demo App: https://github.com/BootcampToProd/spring-ai-structured-output
12. Things to Consider
When working with Spring AI’s Structured Output Converters, keep these important considerations in mind:
- AI Model Compatibility: Not all AI models support structured output equally well. Spring AI has tested compatibility with OpenAI, Anthropic Claude, Azure OpenAI, Mistral AI, Ollama, and Vertex AI Gemini models.
- Response Validation: The AI might not always generate the exact structure you request. Implement validation to ensure the response meets your expectations.
- Prompt Engineering: The quality of your structured output depends on how clearly you communicate your needs in the prompt. Be specific about the structure and content you expect.
- Error Handling: Include proper error handling for cases where the AI generates invalid JSON or misses required fields.
- JSON Mode: Some AI models offer a built-in JSON mode that guarantees valid JSON responses. Spring AI supports these modes through configuration options. More details on this can be found here.
13. FAQs
What is the difference between BeanOutputConverter and MapOutputConverter?
BeanOutputConverter maps AI responses to specific Java classes or records, providing type safety and validation. MapOutputConverter gives you a more flexible Map structure that can handle varying response formats but doesn’t provide type checking.
Can I use Structured Output Converters with any AI model?
While the Spring AI Structured Output Converters work with most models, their effectiveness depends on the model’s ability to follow formatting instructions. Some models are better at following structured output instructions than others.
How do I handle errors when the AI model doesn’t return the expected format?
You should implement proper error handling around your converter calls. Consider using try-catch blocks and providing fallback options or meaningful error messages to the user.
Can I customize the format instructions sent to the AI model?
Yes, you can create custom converters by implementing the StructuredOutputConverter interface and providing your own format instructions and conversion logic.
Can I use complex nested structures with these converters?
Yes! The BeanOutputConverter
can handle complex class hierarchies with nested objects, lists, and maps. Just make sure your Java classes properly represent the structure you want.
14. Conclusion
Spring AI’s Structured Output Converters transform the way we work with AI-generated text. By providing a bridge between natural language AI responses and structured data formats, these converters make it significantly easier to integrate AI capabilities into your Java applications. Whether you need simple JSON outputs, entity mapping, lists of items, or flexible map structures, Spring AI offers converters that fit your needs. This approach not only simplifies your code but also makes your AI interactions more reliable and predictable.
15. Learn More
Interested in learning more?
Spring AI Chat Memory with Cassandra: Building Persistent Conversational Applications
Add a Comment