Structured Output Converter in Spring AI

 Structured Output Converter in Spring AI


Suppose you ask OpenAI:

"Provide the major cities of India."

Normally, AI returns plain text.

India has many major cities such as Mumbai, Delhi, Bengaluru, Hyderabad and Chennai.

In Java, you'll get:

String response = chatClient.prompt()
.user("Provide major cities of India")
.call()
.content();

Type:

String

Problem:

  • It's just text.
  • You have to parse it yourself.
  • AI may change the format every time.

Step 2: Structured Output

Instead of asking AI for plain text, we ask:

"Return the response in a structured format."


{
"country": "India",
"cities": [
"Mumbai",
"Delhi",
"Pune"
]
}


This is called Structured Output.

It means:

AI returns JSON instead of plain text.

 

Step 3: POJO (Plain Old Java Object)

Now create a Java class (or record) to hold this JSON.

public record CountryCities(
String country,
List<String> cities
) {}

This is your POJO/DTO.

It represents:


{ "country":"India", "cities":[...] }



Step 4: StructuredOutputConverter

Spring AI has an interface:StructuredOutputConverter<T>


Purpose:

Convert AI response into a Java object

Think of it like a translator.


AI JSON │ ▼ StructuredOutputConverter │ ▼ Java Object

Methods

public interface StructuredOutputConverter<T> {

T convert(String text);

String getFormat();
}

convert()

Converts

{
 "country":"India"
}

into

CountryCities


getFormat()

Creates instructions for AI.

Example:

Return the response in JSON.

{
  "country":"string",
  "cities":[]
}

Spring AI automatically sends these instructions to OpenAI.


Step 5: BeanOutputConverter

Used when you have a POJO.

Example:

public record CountryCities(
        String country,
        List<String> cities
){}

Create converter

BeanOutputConverter<CountryCities> converter =
        new BeanOutputConverter<>(CountryCities.class);

Flow

Prompt
      │
      ▼
BeanOutputConverter
      │
      ├── getFormat()
      ▼
OpenAI
      ▼
JSON
      ▼
convert()
      ▼
CountryCities


Example

BeanOutputConverter<CountryCities> converter =
new BeanOutputConverter<>(CountryCities.class);

String prompt = """
Provide major cities of India.

%s
""".formatted(converter.getFormat());

String response = chatClient.prompt()
.user(prompt)
.call()
.content();

CountryCities obj = converter.convert(response);BeanOutputConverter<CountryCities> converter =
        new BeanOutputConverter<>(CountryCities.class);

String prompt = """
Provide major cities of India.

%s
""".formatted(converter.getFormat());

String response = chatClient.prompt()
.user(prompt)
.call()
.content();

CountryCities obj = converter.convert(response);

Step 6: .entity() (Modern Spring AI)

Instead of writing:

BeanOutputConverter<CountryCities> converter =
new BeanOutputConverter<>(CountryCities.class);

Spring AI lets you write


CountryCities obj = chatClient
.prompt()
.user("Major cities of India")
.call()
.entity(CountryCities.class);

Internally it does:

BeanOutputConverter
+
convert()
+
getFormat()

for you.

That's why. entity() is the recommended approach.


Step 7: ListOutputConverter

Suppose you only need city names.

AI response:

[
 "Mumbai",
 "Delhi",
 "Pune"
]

Java type

List<String>

Use

List<String> cities = chatClient
.prompt()
.user("List major cities of India")
.call()
.entity(new ListOutputConverter());

Output

[
 Mumbai,
 Delhi,
 Pune
]


Flow

Prompt
      │
      ▼
ListOutputConverter
      │
      ▼
JSON Array
      │
      ▼
List<String>


Step 8: MapOutputConverter

Suppose every response has different fields.

AI returns

{
 "country":"India",
 "capital":"New Delhi",
 "currency":"Indian Rupee"
}

Instead of creating a POJO

Use

Map<String,Object>

Controller

Map<String,Object> response = chatClient
.prompt()
.user("Give details of India")
.call()
.entity(new MapOutputConverter());

Result

response.get("country");

response.get("capital");

response.get("currency");


Flow

Prompt
      │
      ▼
MapOutputConverter
      │
      ▼
JSON Object
      │
      ▼
Map<String,Object>


Step 9: Bean List

Suppose AI returns

[
 {
   "country":"India",
   "cities":[...]
 },
 {
   "country":"USA",
   "cities":[...]
 }
]

Need

List<CountryCities>

Use

List<CountryCities> list =
chatClient.prompt()
.call()
.entity(new ParameterizedTypeReference<List<CountryCities>>() {});


Complete Picture

                    StructuredOutputConverter<T>
                               ▲
             ┌─────────────────┼─────────────────┐
             │                 │                 │
             │                 │                 │
 BeanOutputConverter   ListOutputConverter   MapOutputConverter
             │                 │                 │
             ▼                 ▼                 ▼
      CountryCities       List<String>    Map<String,Object>


Which one should I use?

RequirementConverterReturn Type
One custom object    BeanOutputConverter or.entity(MyClass.class)   CountryCities
List of strings    ListOutputConverter   List<String>
Dynamic key-value data  MapOutputConverter  Map<String, Object>
List of custom objects  ParameterizedTypeReference<List<MyClass>>   List<MyClass>


Interview Summary (Easy to Remember)

  • POJO/DTO → Java class that stores the AI response.
  • Structured Output → AI returns JSON instead of plain text.
  • StructuredOutputConverter → Interface that converts AI JSON into Java objects.
  • BeanOutputConverter → Converts JSON to a custom POJO like CountryCities.
  • ListOutputConverter → Converts JSON array to List<String>.
  • MapOutputConverter → Converts JSON object to Map<String, Object>.
  • .entity() → Modern Spring AI shortcut that automatically uses the appropriate structured output conversion, so you usually don't need to create a BeanOutputConverter manually.

A simple way to remember them is:

  • Bean = One Java object
  • List = Many strings
  • Map = Dynamic key-value pairs
  • StructuredOutputConverter = Parent interface that all these converters implement
package com.example.openai.controller;

import com.example.openai.model.CountryCities;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;


@RestController
@RequestMapping("/api")
public class StructuredOutputController {


private final ChatClient chatClient;


public StructuredOutputController(ChatClient.Builder builder) {

this.chatClient = builder
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}


/*
1. Bean Output Converter

AI Response:
{
country:"India",
cities:["Mumbai","Delhi"]
}

Converted into:

CountryCities object
*/
@GetMapping("/chat-bean")
public ResponseEntity<CountryCities> chatBean(
@RequestParam String country) {


CountryCities response = chatClient
.prompt()
.user("""
Provide major cities of %s.
Return country name and cities.
""".formatted(country))
.call()
.entity(CountryCities.class);


return ResponseEntity.ok(response);
}




/*
2. List Output Converter

AI Response:

[
"Mumbai",
"Delhi",
"Pune"
]

Converted into:

List<String>
*/
@GetMapping("/chat-list")
public ResponseEntity<List<String>> chatList(
@RequestParam String country) {


List<String> response = chatClient
.prompt()
.user("""
Provide major cities of %s.
Return only city names.
""".formatted(country))
.call()
.entity(new ListOutputConverter());


return ResponseEntity.ok(response);
}





/*
3. Map Output Converter

AI Response:

{
country:"India",
capital:"Delhi",
currency:"INR"
}

Converted into:

Map<String,Object>
*/
@GetMapping("/chat-map")
public ResponseEntity<Map<String,Object>> chatMap(
@RequestParam String country) {


Map<String,Object> response = chatClient
.prompt()
.user("""
Provide details about %s.
Include capital,
currency,
population.
""".formatted(country))
.call()
.entity(new MapOutputConverter());


return ResponseEntity.ok(response);
}





/*
4. List of POJO

AI Response:

[
{
country:"India",
cities:["Mumbai","Delhi"]
},
{
country:"USA",
cities:["New York","Chicago"]
}
]

Converted into:

List<CountryCities>
*/

@GetMapping("/chat-bean-list")
public ResponseEntity<List<CountryCities>> chatBeanList(
@RequestParam String message) {


List<CountryCities> response = chatClient
.prompt()
.user(message)
.call()
.entity(
new ParameterizedTypeReference<List<CountryCities>>() {}
);


return ResponseEntity.ok(response);
}

}

package com.example.openai.model;

import java.util.List;

public record CountryCities(String country, List<String> cities) {
}


Testing -
http://localhost:8080/api/chat-bean?country=India

{ "country": "India", "cities": [ "Mumbai", "Delhi", "Bengaluru", "Hyderabad", "Pune" ] }


http://localhost:8080/api/chat-list?country=India


[ "Mumbai", "Delhi", "Pune", "Chennai" ]


http://localhost:8080/api/chat-map?country=India


{ "country": "India", "capital": "New Delhi", "currency": "Indian Rupee", "population": "1.4 Billion" }
http://localhost:8080/api/chat-bean-list?message=Provide details of India and USA with major cities


[ { "country": "India", "cities": [ "Mumbai", "Delhi", "Pune" ] }, { "country": "USA", "cities": [ "New York", "Chicago" ] } ]


Comments

Popular posts from this blog

Async/await

Two Sum II - Input Array Is Sorted

Comparable Vs. Comparator in Java