Spring AI: Mapping LLM Responses to Java Objects
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
Working with large language models often starts simple. You send a question and get a text response. That works fine if you show the answer to a user, but it becomes a problem when you want to process it in your business logic. Plain text is unstructured. Parsing it is an error-prone task, especially when working with a non-deterministic system, like an LLM.
Spring AI solves this problem with structured output. Instead of returning text, it can ask the model to produce a JSON document. When doing that, Spring AI automatically generates a JSON Schema from a Java class or record, adds it to the prompt, and then maps the model’s response into a Java object, which you can use easily in your application.
And to make it even better, you get all of this by only calling 1 method with a reference to the Java type you want to receive.
Defining a Record for the Response
Let’s start with a simple example. You want to ask the model for information about the current chess world champion. But instead of a text response, you want to receive the following ChessChampion record with first and last name and the year in which they were world champion.
public record ChessChampion(
String first,
String last,
List<Integer> years) {
}
You do that by providing a prompt to a ChatClient instance and calling the entity method with a reference to the ChessChampion record.
ChessChampion champ = chatClient.prompt("Name the current chess world champion.")
.call()
.entity(ChessChampion.class);
When you run this code, Spring AI does several things behind the scenes. It uses Jackson to generate a JSON Schema for your record, tells the model to return a valid JSON document that matches the schema, and includes additional formatting instructions.
Here’s what that full prompt looks like when sent to the LLM:
Name the current chess world champion.
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"first" : {
"type" : "string"
},
"last" : {
"type" : "string"
},
"years" : {
"type" : "array",
"items" : {
"type" : "integer"
}
}
},
"additionalProperties" : false
}```
The model receives this prompt and responds with a JSON object matching the provided schema. The following snippet shows the formatted response. As you can see, the LLM provided a good response, even though the field names don’t clearly define which information they represent.
{
"first": "Magnus",
"last": "Carlsen",
"years": [2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
}
And in the final step, Spring AI uses Jackson to map the JSON document to a ChessChampion instance.
Customizing the JSON Mapping
Sometimes, you need more control over the generated JSON schema. You may want to change or shorten a field name or give the model more context about a field’s meaning. You can do this by using Jackson’s @JsonProperty and @JsonPropertyDescription annotations.
Let’s use them to map first and last to the more expressive field names firstName and lastName, and to add a description to the years field. In general, expressive field names and descriptions explaining the semantics of a field to the LLM improve the quality of the response. So, you should always make sure the JSON schema clearly defines which information you want to retrieve.
public record ChessChampion(
@JsonProperty(value = "firstName")
String first,
@JsonProperty(value = "lastName")
String last,
@JsonPropertyDescription("The years when they were world champions.")
List<Integer> years) {
}
Jackson uses these annotations to create the JSON schema definition, and Spring AI includes it in the prompt. Here’s how the updated prompt looks:
Name the current chess world champion.
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"firstName" : {
"type" : "string"
},
"lastName" : {
"type" : "string"
},
"years" : {
"description" : "The years when they were world champions.",
"type" : "array",
"items" : {
"type" : "integer"
}
}
},
"additionalProperties" : false
}```
Everything else works as in the previous example. The LLM responds with a JSON document, and Spring AI uses JSON to map it to a ChessChampion instance.
Getting Collections of Objects
If you want to retrieve a Collection of objects, you have 2 options. You can either model a nested data structure or specify a parameterized type.
Let’s first model a nested data structure as a record.
public record ChessChampions(List<ChessChampion> champions) {}
The ChessChampions record wraps a list of ChessChampion objects. Spring AI will generate a JSON Schema for this composite structure, telling the model to respond with a JSON object containing an array of champions.
As you can see in the following code snippet, you can use the ChessChampions record in the same way as the simpler ChessChampion record in the previous examples.
ChessChampions champions = chatClient.prompt("Name the world champions in classical chess of the last 10 years.")
.call()
.entity(ChessChampions.class);
And Spring AI handles this is the same way. It uses Jackson to create the schema, extends the prompt, and maps the response.
Name the world champions in classical chess of the last 10 years.
Your response should be in JSON format.
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
Do not include markdown code blocks in your response.
Remove the ```json markdown from the output.
Here is the JSON Schema instance your output must adhere to:
```{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"champions" : {
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"firstName" : {
"type" : "string"
},
"lastName" : {
"type" : "string"
},
"years" : {
"description" : "The years when they were world champions.",
"type" : "array",
"items" : {
"type" : "integer"
}
}
},
"additionalProperties" : false
}
}
},
"additionalProperties" : false
}```
If you don’t want to wrap your list in another record, you can directly request a List of objects using a ParameterizedTypeReference.
ChessChampions champions = chatClient.prompt("Name the world champions in classical chess of the last 10 years.")
.call()
.entity(new ParameterizedTypeReference<List<ChessChampion>>() {});
Both methods work the same way. The only difference is that the first one wraps the list inside a record, while the second one works directly with a Java List.
Practical Notes
Structured output is extremely useful because it bridges the gap between language models and business logic. Instead of parsing plain text, you can work directly with typed Java objects. That makes your code safer and easier to maintain.
You should still treat model responses with some caution. The structure is guaranteed, but the facts are not. A model can return valid JSON that contains incorrect or incomplete information. It’s therefore a good idea to validate the returned data. I will talk about that in more detail in a future post.
During development, it’s also helpful to log the raw JSON responses. Seeing the exact output helps you adjust your records, simplify and comment your schemas, or refine your prompts.
Summary
Structured output in Spring AI makes model responses predictable and type-safe. You define a class or record, and Spring AI generates a JSON Schema and instructs the model to return a matching JSON document. Spring AI then maps the JSON to your Java type.
Jackson provides a reliable default mapping for Java records and classes. If you want to, you can customize field names using Jackson’s @JsonProperty annotation and provide additional information to the LLM by providing a field description via a @JsonPropertyDescription annotation.
And if you want to retrieve a Collection of objects, you can either wrap it in another data structure or use a ParameterizedTypeDescription to specify the requested type.

