Core model interfaces and abstractions for Spring AI framework providing portable API for chat, embeddings, images, audio, and tool calling across multiple AI providers
Framework for converting LLM text outputs into structured Java objects using JSON schema, supporting POJOs, lists, maps, and custom converters with response text cleaning utilities.
Main interface for converting text to structured objects.
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
/**
* Convert text to the target type.
*
* @param text the text to convert
* @return the converted object
*/
T convert(String text);
/**
* Get format instructions for the LLM.
* These instructions tell the LLM how to format its output.
*
* @return the format instructions
*/
String getFormat();
}Provides format instructions for LLMs.
public interface FormatProvider {
/**
* Get format instructions for output formatting.
*
* @return the format instructions
*/
String getFormat();
}Converts text to POJOs using JSON schema.
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
/**
* Construct a BeanOutputConverter for a class.
*
* @param clazz the target class
*/
public BeanOutputConverter(Class<T> clazz);
/**
* Construct a BeanOutputConverter with custom ObjectMapper.
*
* @param clazz the target class
* @param objectMapper the Jackson ObjectMapper
*/
public BeanOutputConverter(Class<T> clazz, ObjectMapper objectMapper);
/**
* Construct a BeanOutputConverter for a parameterized type.
*
* @param typeRef the parameterized type reference
*/
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef);
/**
* Construct a BeanOutputConverter with type reference and ObjectMapper.
*
* @param typeRef the parameterized type reference
* @param objectMapper the Jackson ObjectMapper
*/
public BeanOutputConverter(ParameterizedTypeReference<T> typeRef, ObjectMapper objectMapper);
/**
* Convert text to bean.
*
* @param text the JSON text
* @return the bean instance
*/
T convert(String text);
/**
* Get format instructions with JSON schema.
*
* @return the format instructions
*/
String getFormat();
/**
* Get the JSON schema for the target type.
*
* @return the JSON schema string
*/
String getJsonSchema();
/**
* Get the JSON schema as a map.
*
* @return the schema map
*/
Map<String, Object> getJsonSchemaMap();
}Converts text to lists of strings.
public class ListOutputConverter extends AbstractConversionServiceOutputConverter<List<String>> {
/**
* Construct a ListOutputConverter with ConversionService.
*
* @param conversionService the Spring ConversionService
*/
public ListOutputConverter(ConversionService conversionService);
/**
* Convert text to list of strings.
*
* @param text the text (comma-separated, JSON array, etc.)
* @return list of strings
*/
List<String> convert(String text);
}Converts text to maps.
public class MapOutputConverter extends AbstractMessageOutputConverter<Map<String, Object>> {
/**
* Construct a MapOutputConverter.
*/
public MapOutputConverter();
/**
* Convert text to map.
*
* @param text the JSON text
* @return the map
*/
Map<String, Object> convert(String text);
}Base class for converters using Spring's ConversionService.
public abstract class AbstractConversionServiceOutputConverter<T>
implements StructuredOutputConverter<T> {
// Base implementation with ConversionService support
}Base class for message-based output converters.
public abstract class AbstractMessageOutputConverter<T>
implements StructuredOutputConverter<T> {
// Base implementation for message conversion
}Cleans response text before conversion.
public interface ResponseTextCleaner {
/**
* Clean the response text.
*
* @param text the raw text
* @return the cleaned text
*/
String clean(String text);
}Removes markdown code block delimiters.
public class MarkdownCodeBlockCleaner implements ResponseTextCleaner {
/**
* Remove markdown code blocks (```json, ```, etc.).
*
* @param text the text with markdown
* @return the text without markdown delimiters
*/
String clean(String text);
}Removes thinking tags from responses.
public class ThinkingTagCleaner implements ResponseTextCleaner {
/**
* Remove <thinking> tags and their content.
*
* @param text the text with thinking tags
* @return the text without thinking tags
*/
String clean(String text);
}Trims whitespace from responses.
public class WhitespaceCleaner implements ResponseTextCleaner {
/**
* Trim leading and trailing whitespace.
*
* @param text the text
* @return the trimmed text
*/
String clean(String text);
}Combines multiple cleaners.
public class CompositeResponseTextCleaner implements ResponseTextCleaner {
/**
* Construct a CompositeResponseTextCleaner.
*
* @param cleaners the list of cleaners to apply in order
*/
public CompositeResponseTextCleaner(List<ResponseTextCleaner> cleaners);
/**
* Apply all cleaners in sequence.
*
* @param text the text to clean
* @return the cleaned text
*/
String clean(String text);
/**
* Create a new builder.
*
* @return a new builder
*/
static Builder builder();
}import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
// Define target class
record Person(String name, int age, String occupation) {}
// Create converter
BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);
// Create prompt with format instructions
String template = """
Extract person information from this text: {text}
{format}
""";
PromptTemplate promptTemplate = new PromptTemplate(template);
promptTemplate.add("text", "John Smith is a 35-year-old software engineer");
promptTemplate.add("format", converter.getFormat());
Prompt prompt = promptTemplate.create();
// Get response
ChatResponse response = chatModel.call(prompt);
String responseText = response.getResult().getOutput().getText();
// Convert to POJO
Person person = converter.convert(responseText);
System.out.println(person.name()); // "John Smith"
System.out.println(person.age()); // 35// Complex nested class
record Address(String street, String city, String country) {}
record Contact(String email, String phone) {}
record Employee(
String name,
Address address,
Contact contact,
List<String> skills
) {}
// Create converter
BeanOutputConverter<Employee> converter = new BeanOutputConverter<>(Employee.class);
// Use in prompt
String prompt = """
Extract employee information and format as:
%s
""".formatted(converter.getFormat());
// Parse response
Employee employee = converter.convert(responseText);import org.springframework.core.ParameterizedTypeReference;
// List of POJOs
ParameterizedTypeReference<List<Person>> listType =
new ParameterizedTypeReference<>() {};
BeanOutputConverter<List<Person>> listConverter =
new BeanOutputConverter<>(listType);
// Map of POJOs
ParameterizedTypeReference<Map<String, Person>> mapType =
new ParameterizedTypeReference<>() {};
BeanOutputConverter<Map<String, Person>> mapConverter =
new BeanOutputConverter<>(mapType);
// Use converters
List<Person> people = listConverter.convert(responseText);
Map<String, Person> personMap = mapConverter.convert(responseText);import org.springframework.ai.converter.ListOutputConverter;
ListOutputConverter converter = new ListOutputConverter(conversionService);
// Convert comma-separated
String response1 = "apple, banana, orange";
List<String> fruits1 = converter.convert(response1);
// Convert JSON array
String response2 = "[\"apple\", \"banana\", \"orange\"]";
List<String> fruits2 = converter.convert(response2);
// Use in prompt
String prompt = "List 5 programming languages. " + converter.getFormat();import org.springframework.ai.converter.MapOutputConverter;
MapOutputConverter converter = new MapOutputConverter();
// Convert JSON object
String response = """
{
"name": "Alice",
"age": 30,
"city": "New York"
}
""";
Map<String, Object> data = converter.convert(response);
System.out.println(data.get("name")); // "Alice"
System.out.println(data.get("age")); // 30import org.springframework.ai.converter.MarkdownCodeBlockCleaner;
MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
// Input with markdown
String response = """
```json
{
"name": "John",
"age": 30
}
```
""";
// Clean it
String cleaned = cleaner.clean(response);
// Result: {"name": "John", "age": 30}
// Then convert
BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);
Person person = converter.convert(cleaned);import org.springframework.ai.converter.*;
// Build composite cleaner
CompositeResponseTextCleaner cleaner = CompositeResponseTextCleaner.builder()
.add(new ThinkingTagCleaner())
.add(new MarkdownCodeBlockCleaner())
.add(new WhitespaceCleaner())
.build();
// Clean complex response
String response = """
<thinking>
The user wants JSON format...
</thinking>
```json
{"name": "Bob", "age": 25}
```
""";
String cleaned = cleaner.clean(response);
// Result: {"name": "Bob", "age": 25}public class CustomConverter implements StructuredOutputConverter<MyType> {
@Override
public MyType convert(String text) {
// Custom parsing logic
return parseMyType(text);
}
@Override
public String getFormat() {
return "Format your response as: field1=value1, field2=value2";
}
private MyType parseMyType(String text) {
// Custom implementation
String[] parts = text.split(",");
// Parse and return MyType
return new MyType(parts);
}
}@Service
public class StructuredOutputService {
private final ChatModel chatModel;
private final CompositeResponseTextCleaner cleaner;
public StructuredOutputService(ChatModel chatModel) {
this.chatModel = chatModel;
this.cleaner = CompositeResponseTextCleaner.builder()
.add(new ThinkingTagCleaner())
.add(new MarkdownCodeBlockCleaner())
.add(new WhitespaceCleaner())
.build();
}
public <T> T extractStructured(String prompt, Class<T> targetClass) {
// Create converter
BeanOutputConverter<T> converter = new BeanOutputConverter<>(targetClass);
// Add format instructions
String fullPrompt = prompt + "\n\n" + converter.getFormat();
// Get response
String response = chatModel.call(fullPrompt);
// Clean response
String cleaned = cleaner.clean(response);
// Convert to object
return converter.convert(cleaned);
}
}BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);
// Get JSON schema as string
String schemaString = converter.getJsonSchema();
System.out.println(schemaString);
// Get JSON schema as map
Map<String, Object> schemaMap = converter.getJsonSchemaMap();
System.out.println(schemaMap.get("properties"));
// Use schema for validation or documentationpublic <T> T safeConvert(String text, Class<T> clazz) {
try {
BeanOutputConverter<T> converter = new BeanOutputConverter<>(clazz);
// Clean text first
CompositeResponseTextCleaner cleaner = CompositeResponseTextCleaner.builder()
.add(new MarkdownCodeBlockCleaner())
.add(new WhitespaceCleaner())
.build();
String cleaned = cleaner.clean(text);
// Convert
return converter.convert(cleaned);
} catch (Exception e) {
System.err.println("Conversion failed: " + e.getMessage());
return null;
}
}enum Status { ACTIVE, INACTIVE, PENDING }
record Task(
String name,
Status status,
int priority
) {}
BeanOutputConverter<Task> converter = new BeanOutputConverter<>(Task.class);
// The schema will include enum constraints
String schema = converter.getJsonSchema();
// Will specify that status must be one of: ACTIVE, INACTIVE, PENDING
// Convert response
Task task = converter.convert(responseText);
System.out.println(task.status()); // Status.ACTIVErecord Tag(String name, String color) {}
record Article(
String title,
String content,
List<Tag> tags,
Map<String, String> metadata
) {}
BeanOutputConverter<Article> converter = new BeanOutputConverter<>(Article.class);
// Schema will handle nested structure
String prompt = """
Extract article information:
%s
""".formatted(converter.getFormat());
Article article = converter.convert(response);
System.out.println(article.tags().size());@Service
public class ExtractionService {
private final ChatModel chatModel;
public Person extractPerson(String text) {
BeanOutputConverter<Person> converter = new BeanOutputConverter<>(Person.class);
PromptTemplate template = new PromptTemplate("""
Extract person information from this text: {input}
Return your response in this format:
{format}
""");
template.add("input", text);
template.add("format", converter.getFormat());
Prompt prompt = template.create();
String response = chatModel.call(prompt)
.getResult()
.getOutput()
.getText();
return converter.convert(response);
}
}import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
// Configure ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Use with converter
BeanOutputConverter<MyClass> converter =
new BeanOutputConverter<>(MyClass.class, objectMapper);
// Now supports Java 8 date/time types and ignores unknown properties@Configuration
public class ConverterConfig {
@Bean
public CompositeResponseTextCleaner responseTextCleaner() {
return CompositeResponseTextCleaner.builder()
.add(new ThinkingTagCleaner())
.add(new MarkdownCodeBlockCleaner())
.add(new WhitespaceCleaner())
.build();
}
@Bean
public ObjectMapper converterObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}