ai 3 min read

Structured AI Responses in Spring Boot with Kotlin

Master structured responses in Spring AI with Kotlin to get type-safe, predictable outputs from AI models. Learn how to map AI responses to your domain objects and handle them efficiently in your Spring Boot applications.

Working with AI responses can be tricky when you need specific data structures in your application. In this guide, we'll explore how to get structured, type-safe responses from AI models using Spring AI and Kotlin.

Key Takeaways

  • Learn how to map AI responses to type-safe Kotlin data classes
  • Understand how to use ParameterizedTypeReference for type safety
  • Implement reusable AI response structures
  • Handle structured responses efficiently in Spring Boot

Prerequisites

  • Basic knowledge of Kotlin and Spring Boot
  • Familiarity with Spring AI basics (see our getting started guide)
  • OpenAI API key

Understanding Structured Responses

When working with AI models, getting free-form text responses isn't always ideal. Sometimes you need:

  • Specific data formats
  • Type-safe responses
  • Consistent response structures
  • Easy integration with your domain model

Let's see how to achieve this using Spring AI.

Implementation

First, let's create a data class for our structured response:

data class InstructionResponse(
    val response: String,
    val reasoning: String
)

Now, let's implement the AI client that returns structured responses:

class StructuredAIClient(
    apiKey: String,
    modelName: String = "gpt-4"
) {
    private val chatModel = OpenAiChatModel(
        OpenAiApi(apiKey),
        OpenAiChatOptions().apply {
            model = modelName
            temperature = 0.5  // Lower temperature for more consistent outputs
        }
    )

    private val client = ChatClient.create(chatModel)

    fun getStructuredResponse(input: String): InstructionResponse {
        val prompt = client.prompt("Before giving your final response, add some reasoning on how you got to that response.")
        return prompt.call().entity(
            object : ParameterizedTypeReference<InstructionResponse>() {}
        )
    }
}

Using the Structured Response Client

Here's how to use the client in your application:

@Service
class AIService(
    @Value("\${openai.api-key}")
    private val apiKey: String
) {
    private val aiClient = StructuredAIClient(apiKey)

    fun getInstructions(query: String): InstructionResponse {
        return aiClient.getStructuredResponse(query)
    }
}

And in your controller:

@RestController
@RequestMapping("/api/ai")
class AIController(private val aiService: AIService) {
    
    @PostMapping("/instructions")
    fun getInstructions(@RequestBody query: String): InstructionResponse {
        return aiService.getInstructions(query)
    }
}

Benefits of Structured Responses

  1. Type Safety: By mapping responses to Kotlin data classes, you get compile-time type checking.
  2. Predictable Format: Your application always receives data in a known structure.
  3. Easy Integration: Structured responses can be easily integrated with your domain model.
  4. Better Error Handling: Type-safe responses make it easier to handle errors and edge cases.

Advanced Usage: Generic Type Parameters

For more flexibility, you can create a generic method that handles different response types:

class StructuredAIClient(apiKey: String) {
    // ... previous initialization code ...

    fun <T> getTypedResponse(
        input: String,
        typeReference: ParameterizedTypeReference<T>
    ): T {
        val prompt = client.prompt(input)
        return prompt.call().entity(typeReference)
    }
}

This allows you to handle multiple response types:

data class ProductSuggestion(
    val name: String,
    val price: Double,
    val features: List<String>
)

// Usage
val suggestion = aiClient.getTypedResponse(
    "Suggest a product under $100",
    object : ParameterizedTypeReference<ProductSuggestion>() {}
)

Best Practices

  1. Clear Prompts: Always provide clear instructions in your prompts about the expected response format.
  2. Validation: Add validation to ensure the AI response matches your expected structure.
  3. Error Handling: Implement proper error handling for cases where the AI response doesn't match your data class.
  4. Temperature Setting: Use lower temperature values (0.1-0.5) for more consistent structured outputs.

Common Issues and Solutions

Issue: Inconsistent JSON Format

Sometimes the AI might return slightly different JSON structures. Handle this by:

@JsonIgnoreProperties(ignoreUnknown = true)
data class InstructionResponse(
    val response: String,
    val reasoning: String
)

Issue: Parsing Errors

Implement proper error handling:

try {
    return prompt.call().entity(typeReference)
} catch (e: Exception) {
    logger.error("Failed to parse AI response", e)
    throw AIParsingException("Failed to parse AI response: ${e.message}")
}

Conclusion

Structured responses from AI models help maintain type safety and predictability in your Kotlin applications. By using Spring AI's features and Kotlin's type system, you can create robust AI-powered applications that are easier to maintain and debug.

Remember to check our OpenAI integration guide for more details on setting up Spring AI with OpenAI.

Accompanying Code

You can find the code accompanying this blog post on github.

Further Reading