Output Parsers & Structured Output

    Transform unstructured AI responses into type-safe Java objects. Spring AI's structured output capabilities bridge the gap between free-form LLM text and your application's data models.

    Large language models generate text, but enterprise applications need structured data—POJOs, records, lists, and maps that can be validated, stored in databases, and passed through APIs. Output parsers solve this fundamental impedance mismatch by instructing the model to respond in a specific format (usually JSON) and then deserializing that response into strongly-typed Java objects.

    Spring AI makes this remarkably simple with the .entity() method. Under the hood, it generates a JSON Schema from your Java class, injects format instructions into the prompt, and uses Jackson to deserialize the response. You get compile-time type safety with zero boilerplate.

    Why Structured Output Matters

    Type Safety

    Catch errors at compile time, not runtime. Your IDE provides autocomplete, and refactoring propagates through your codebase. No more parsing JSON strings manually.

    Database Ready

    Parsed objects can be directly persisted with JPA/Hibernate. Map AI-extracted entities to your domain model and save them without intermediate transformations.

    API Integration

    Return structured objects directly from REST controllers. Spring automatically serializes them to JSON for your frontend or downstream services.

    Basic Entity Parsing

    The .entity() Method

    The simplest way to get structured output. Pass a class, get an instance back.

    Step 1: Define Your Data Model

    Use Java records for clean, immutable data structures. Spring AI inspects the record components to generate the JSON Schema.

    Product.java
    // Simple record - Spring AI will generate a JSON Schema from thispublicrecordProduct(String name,String description,double price,String category,List<String> features
    ){}

    Step 2: Call .entity() with Your Class

    ProductExtractor.java
    @ServicepublicclassProductExtractor{privatefinalChatClient chatClient;publicProductExtractor(ChatClient.Builder builder){this.chatClient = builder.build();}publicProductextractProduct(String productDescription){return chatClient.prompt().user("Extract product details from: "+ productDescription).call().entity(Product.class);// Magic happens here!}}

    What happens under the hood: Spring AI generates JSON Schema from Product.class, appends format instructions to your prompt, and deserializes the JSON response using Jackson.

    Parsing Lists and Collections

    ParameterizedTypeReference for Generics

    Due to Java type erasure, you need ParameterizedTypeReference to parse generic types like List<T>.

    Collection Parsing
    @ServicepublicclassKeywordService{privatefinalChatClient chatClient;publicKeywordService(ChatClient.Builder builder){this.chatClient = builder.build();}// Extract a list of stringspublicList<String>extractKeywords(String article){return chatClient.prompt().user("""
    Extract 5-10 keywords from this article.
    Return only the keywords as a JSON array of strings.
    Article: %s
    """.formatted(article)).call().entity(newParameterizedTypeReference<List<String>>(){});}// Extract a list of complex objectspublicList<Product>extractProducts(String catalog){return chatClient.prompt().user("Extract all products from this catalog: "+ catalog).call().entity(newParameterizedTypeReference<List<Product>>(){});}}

    Complex Nested Structures

    Multi-Level Object Hierarchies

    Spring AI handles nested records, enums, and complex type hierarchies automatically.

    Complex Nested Structure
    // Nested records with enumspublicrecordAnalysisResult(Summary summary,List<Issue> issues,Recommendation recommendation
    ){}publicrecordSummary(String overview,int totalScore,Severity overallSeverity
    ){}publicrecordIssue(String description,String location,Severity severity,String suggestedFix
    ){}publicrecordRecommendation(Priority priority,List<String> actionItems,String estimatedEffort
    ){}publicenumSeverity{LOW,MEDIUM,HIGH,CRITICAL}publicenumPriority{IMMEDIATE,SHORT_TERM,LONG_TERM}// Usage@ServicepublicclassCodeReviewService{privatefinalChatClient chatClient;publicAnalysisResultanalyzeCode(String code,String language){return chatClient.prompt().system("You are a senior code reviewer. Be thorough but constructive.").user("Analyze this "+ language +" code for bugs and improvements:\n"+ code).call().entity(AnalysisResult.class);}}

    Error Handling & Validation

    LLMs can produce malformed JSON or values that don't match your schema. Always wrap parsing in try-catch and consider using validation annotations.

    Error Handling with Validation
    importjakarta.validation.constraints.*;// Add validation annotations to your recordspublicrecordValidatedProduct(@NotBlankString name,@Size(min =10, max =500)String description,@Positivedouble price,@NotNullCategory category
    ){}@ServicepublicclassSafeProductExtractor{privatefinalChatClient chatClient;privatefinalValidator validator;publicOptional<ValidatedProduct>extractProductSafely(String text){try{ValidatedProduct product = chatClient.prompt().user("Extract product from: "+ text).call().entity(ValidatedProduct.class);// Validate the parsed objectSet<ConstraintViolation<ValidatedProduct>> violations = 
    validator.validate(product);if(violations.isEmpty()){returnOptional.of(product);}else{
    log.warn("Validation failed: {}", violations);returnOptional.empty();}}catch(Exception e){
    log.error("Failed to parse product: {}", e.getMessage());returnOptional.empty();}}}

    BeanOutputParser (Explicit Control)

    Manual Format Instructions

    When you need explicit control over the format instructions sent to the model.

    BeanOutputParser Explicit Usage
    @ServicepublicclassExplicitParsingService{privatefinalChatClient chatClient;publicPersonextractPerson(String biography){// Create parser explicitlyBeanOutputParser<Person> parser =newBeanOutputParser<>(Person.class);// Get the format instructions (JSON Schema)String formatInstructions = parser.getFormat();// Include format in prompt explicitlyString response = chatClient.prompt().user(u -> u.text("""
    Extract person information from this biography:
    {biography}
    Respond with a JSON object matching this schema:
    {format}
    """).param("biography", biography).param("format", formatInstructions)).call().content();// Get raw string// Parse manually (useful for debugging)return parser.parse(response);}}

    When to use explicit parsing: Debugging, custom format instructions, logging the raw response before parsing, or when you need to modify the schema.

    Best Practices

    ✓ Do

    • Use records for immutable, concise data models
    • Add validation annotations (@NotNull, @Size, etc.)
    • Use enums for constrained values (status, category)
    • Wrap parsing in try-catch for graceful failure
    • Set low temperature (0.0-0.3) for consistent output

    ✗ Avoid

    • Overly complex schemas (>10 fields per object)
    • Deep nesting (>3 levels) without chunking
    • Assuming 100% parse success—always handle failures
    • Ignoring model limits (some can't do complex JSON)
    • High temperature for structured output (causes errors)

    Build Type-Safe AI Applications

    Structured output is the bridge between AI and production software. Combine it with prompt engineering for reliable, enterprise-grade applications.