Course topics

By WebNest Studio

Spring Boot Tutorial

Structured Output with Spring AI

Plain text answers are fine for a chatbot, but most business features need data: a list of tags, a sentiment score, an extracted invoice, a classification label. Parsing free text with string operations is fragile. Spring AI's structured output support asks the model to reply in JSON that matches a schema generated from your Java type, and converts the reply into a Java object for you.

This lesson covers mapping to records, lists and maps, using enums for classification, extracting data from unstructured text, validating the result, and the settings that make structured output reliable.

How .entity() Works

When you call .entity(MyRecord.class), Spring AI's BeanOutputConverter generates a JSON Schema from the record, appends formatting instructions to your prompt, and parses the model's JSON reply into an instance of the record using Jackson. For models and providers that support it, Spring AI can also use the provider's native structured output mode, where the model is constrained to produce valid JSON for the schema.

Designing Types for the Model

The model only sees the schema, so make it self-explanatory. Use descriptive field names, @JsonPropertyDescription to explain fields, enums for fixed sets of values, and simple types (strings, numbers, booleans, lists, nested records). Keep objects small; asking for forty fields at once increases errors.

Generic Types: Lists and Maps

Java erases generics at runtime, so List<Product>.class is not possible. Use new ParameterizedTypeReference<List<Product>>() {} instead. For ad-hoc data you can map to Map<String, Object>, but typed records are safer.

Validation and Retries

Even with a schema, a model can return values that are syntactically valid but wrong — a negative quantity, a missing required field, an invalid email. Apply Bean Validation to the result, and for important flows retry or fall back when validation fails. Spring AI 2.0 includes a StructuredOutputValidationAdvisor that checks the reply against the schema and asks the model to correct non-conforming output automatically.

Temperature and Determinism

For extraction and classification, set temperature to 0 or close to it. You want the same input to produce the same output every time, and creative variation only introduces errors.

Examples

Mapping an answer to a record

Java
public record CourseOutline(
        String title,
        @JsonPropertyDescription("Target learner level: BEGINNER, INTERMEDIATE or ADVANCED") Level level,
        @JsonPropertyDescription("5 to 8 lesson titles in teaching order") List<String> lessons,
        @JsonPropertyDescription("Estimated total hours") int hours) {

    public enum Level { BEGINNER, INTERMEDIATE, ADVANCED }
}

CourseOutline outline = chatClient.prompt()
    .user("Design a short course on Spring Data JPA for developers who know SQL.")
    .call()
    .entity(CourseOutline.class);

System.out.println(outline.title() + " (" + outline.level() + ", " + outline.hours() + "h)");
outline.lessons().forEach(l -> System.out.println(" - " + l));
Output
Spring Data JPA for SQL Developers (INTERMEDIATE, 6h)
 - Entities and the persistence context
 - Repositories and derived queries
 - JPQL and native queries
 - Relationships and fetching
 - Transactions
 - Performance and the N+1 problem

Classifying support tickets with an enum

Java
public enum TicketCategory { BILLING, LOGIN_PROBLEM, COURSE_CONTENT, BUG_REPORT, OTHER }

public record TicketTriage(TicketCategory category,
                           @JsonPropertyDescription("1 = low, 5 = urgent") int priority,
                           String summary) {}

@Service
public class TriageService {

    private final ChatClient chatClient;

    public TriageService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("Classify customer support tickets for an online learning platform.")
            .defaultOptions(OpenAiChatOptions.builder().temperature(0.0).build())
            .build();
    }

    public TicketTriage triage(String ticketText) {
        return chatClient.prompt().user(ticketText).call().entity(TicketTriage.class);
    }
}
Output
triage("I was charged twice for the Spring Boot course and I can't find a refund option!!")
TicketTriage[category=BILLING, priority=4, summary=Customer was double-charged for the Spring Boot course and wants a refund.]

Extracting a list of objects from unstructured text

Java
public record LineItem(String product, int quantity, BigDecimal unitPrice) {}

String email = """
    Hi, please send 3 Java Core workbooks at 499 each,
    one Spring Boot hoodie (1299) and 2 stickers packs for 99 each. Thanks, Ravi
    """;

List<LineItem> items = chatClient.prompt()
    .system("Extract ordered items. Prices are in INR.")
    .user(email)
    .call()
    .entity(new ParameterizedTypeReference<List<LineItem>>() {});

items.forEach(System.out::println);
Output
LineItem[product=Java Core workbook, quantity=3, unitPrice=499]
LineItem[product=Spring Boot hoodie, quantity=1, unitPrice=1299]
LineItem[product=Sticker pack, quantity=2, unitPrice=99]

Validating structured output before using it

Java
public record Signup(@NotBlank String name, @Email String email, @Min(13) int age) {}

@Service
public class SignupExtractor {

    private final ChatClient chatClient;
    private final Validator validator;

    public SignupExtractor(ChatClient.Builder builder, Validator validator) {
        this.chatClient = builder.build();
        this.validator = validator;
    }

    public Signup extract(String text) {
        Signup signup = chatClient.prompt()
            .user(u -> u.text("Extract the signup details from: {text}").param("text", text))
            .call()
            .entity(Signup.class);

        Set<ConstraintViolation<Signup>> errors = validator.validate(signup);
        if (!errors.isEmpty()) {
            throw new IllegalArgumentException("AI extraction failed validation: " + errors);
        }
        return signup;
    }
}
Output
extract("I'm Meera, 24, reach me at meera@example.com") -> Signup[name=Meera, email=meera@example.com, age=24]
extract("Name: Tom, email: not-an-email, age 9")         -> IllegalArgumentException: AI extraction failed validation: [email must be a well-formed email address, age must be greater than or equal to 13]

Common Mistakes

  • Parsing JSON out of content() manually with regular expressions instead of using entity().
  • Using List<MyType>.class style code (impossible) instead of ParameterizedTypeReference for generic types.
  • Giving fields vague names like "value" or "data" so the model cannot infer what to put in them.
  • Trusting extracted values without validation, especially numbers, dates and emails.
  • Using a high temperature for extraction, producing different results for the same input.

Key Points to Remember

  • .entity(Class) generates a JSON schema, instructs the model and converts the reply to your type.
  • Use ParameterizedTypeReference for lists and other generic types.
  • Descriptive names, @JsonPropertyDescription and enums make results far more accurate.
  • Validate results with Bean Validation and use low temperature for extraction and classification.
  • StructuredOutputValidationAdvisor can automatically ask the model to fix non-conforming JSON.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.