Using Java records as the target type for LLM structured outputs gives you type-safe, immutable, concisely-declared AI response objects — replacing manual JSON parsing and string extraction with compiler-verified types.
Why It's in Trial
Java records (introduced in Java 16, now standard) are the ideal data carrier for structured LLM outputs. They are:
- Immutable — LLM responses shouldn't mutate
- Concise — no boilerplate getters/setters/constructors
- JSON-friendly — Jackson serialises/deserialises records without configuration
- Transparent — record components are part of the type definition; easy to read
Combined with Spring AI's structured output support or LangChain4j's @StructuredPrompt, this pattern eliminates a whole class of brittle string-parsing code.
Spring AI: Entity Extraction
// Define the shape of the response
record ProductAnalysis(
String sentiment, // "positive" | "negative" | "neutral"
List<String> keyFeatures,
int confidenceScore, // 0–100
String recommendedAction
) {}
// Ask the model to fill it
ProductAnalysis analysis = chatClient.prompt()
.user("Analyse this product review: " + reviewText)
.call()
.entity(ProductAnalysis.class);
// Fully type-safe from here
if (analysis.confidenceScore() > 80 && "negative".equals(analysis.sentiment())) {
escalateToSupportTeam(analysis.recommendedAction());
}
Spring AI generates the JSON schema from the record definition and instructs the model to respond conforming to that schema. No manual schema writing.
LangChain4j: @StructuredPrompt
@StructuredPrompt("Extract the key details from this support ticket: {{ticket}}")
record SupportTicket(
String summary,
String priority, // "low" | "medium" | "high" | "critical"
List<String> affectedSystems,
String suggestedTeam
) {}
// In your AI service
interface TicketRouter {
SupportTicket extract(String ticket);
}
TicketRouter router = AiServices.create(TicketRouter.class, model);
SupportTicket ticket = router.extract(rawTicketText);
Sealed Interfaces for Discriminated Unions
For AI responses that can be one of several shapes, sealed interfaces + records model the type precisely:
sealed interface ClassificationResult permits
Approved, Rejected, NeedsReview {}
record Approved(String reason, double confidence) implements ClassificationResult {}
record Rejected(String reason, List<String> violations) implements ClassificationResult {}
record NeedsReview(String reason, String assignedTeam) implements ClassificationResult {}
Pattern matching in switch then handles each case exhaustively — the compiler enforces that you've handled every possible LLM response shape.
Validation
Pair with Bean Validation to catch model responses that are structurally valid JSON but semantically wrong:
record SentimentScore(
@NotBlank String sentiment,
@Min(0) @Max(100) int confidence,
@Size(min = 1, max = 5) List<String> reasons
) {}
Spring AI validates structured outputs automatically when Bean Validation is on the classpath.
Key Characteristics
| Property | Value |
|---|---|
| Requires | Java 16+ (records GA) |
| Spring AI support | ChatClient.call().entity(MyRecord.class) |
| LangChain4j support | @StructuredPrompt + AiServices |
| Schema generation | Automatic from record definition |