LangChain4j Structured Output
Return typed objects from LangChain4j AI Services: POJO and record return types, enums, lists, JSON schema mode and validation — no manual parsing of model responses.
On this page
Structured output is where LangChain4j's declarative style pays off most. You declare a return type on an interface method, and the library derives the schema, constrains the model and deserialises the response. This is how you wire a model into typed application code instead of parsing text by hand.
Key Takeaways
- The return type drives everything — records, enums, lists, booleans all work.
- Enums constrain the model to your permitted values; prefer them over strings.
- With a supporting provider, JSON schema mode guarantees structural validity.
- Structured output guarantees shape, not correctness — validate the values yourself.
Typed return values
The return type of the interface method is the contract:
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
record Ticket(String summary, Priority priority, List<String> components) {
enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
}
interface Analyst {
// Enum — constrained to three values.
Sentiment sentimentOf(String review);
// Record — schema derived from the fields.
Ticket triage(String bugReport);
// Boolean — trivial routing logic.
boolean isSpam(String message);
// List of records.
List<Ticket> triageAll(String bugReports);
}Analyst analyst = AiServices.create(Analyst.class, chatModel);
if (analyst.isSpam(message)) return;
Ticket ticket = analyst.triage(message); // a typed object, no parsingCompare that to parsing free-form text into a Ticket, with retries for malformed JSON. The library
handles schema generation, the instruction and deserialisation.
Enums: the most reliable structured output
An enum return type is exceptionally reliable because the schema lists the only valid answers:
enum Intent { BILLING, TECHNICAL, SALES, OTHER }
interface Router {
@UserMessage("Classify the intent of: {{it}}")
Intent classify(String message);
}The model cannot return REFUND_REQUEST — it is not in the enum, so the schema forbids it.
JSON schema mode
On providers that support it, LangChain4j can use strict JSON schema mode, which constrains the output at the API level rather than through prompt instructions:
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
// Ask the provider to enforce the response schema, not just request it.
.responseFormat("json_schema")
.strictJsonSchema(true)
.build();This raises structural reliability from "very high with a clear type" to "guaranteed valid against the schema", which matters for high-volume extraction where even a rare parse failure is a real cost.
Validating values
Structured output guarantees the object matches your type. It does not guarantee the values are correct — that is a separate concern:
record DateRange(LocalDate from, LocalDate to) {
public DateRange {
// The model returned two valid dates. That does not make them ordered.
if (to.isBefore(from)) {
throw new IllegalArgumentException("end date before start date");
}
}
}Handling parse failures
Rare with a clear type and low temperature, but not zero. Handle it:
public Optional<Ticket> triageSafely(String report) {
try {
return Optional.of(analyst.triage(report));
} catch (Exception e) {
log.warn("triage parse failed for report length {}", report.length(), e);
return Optional.empty(); // degrade rather than crash the request
}
}Designing types the model can fill
The field names are part of the prompt — the model reads them. Design for the model, not only for your database:
- Descriptive names —
prepMinutesextracts better thanpm. - Flatter structures — deeply nested types are harder to fill correctly.
- Enums for closed sets — as above.
- Nullable for genuinely optional fields — with an instruction to leave unknowns null rather than guess.
record Invoice(
String vendor,
LocalDate date,
List<LineItem> items,
BigDecimal total) {
record LineItem(String description, int quantity, BigDecimal unitPrice) {}
}When to use structured output
Use it whenever your code consumes the response — extraction, classification, routing, anything you
if or switch on. Use plain text return only when a human reads the output directly. The rule of
thumb: if the machine acts on it, type it.
Next
You have completed Phase 2. You know LangChain4j end to end.
- What is Agentic AI? — Phase 3 begins
- Agent architecture patterns
Frequently Asked Questions
How does LangChain4j return a typed object instead of text?
Can LangChain4j guarantee valid JSON from the model?
Should I use an enum or a string for a category field?
How do I validate the values, not just the structure?
Related tutorials
- LangChain4j Chat MemoryAdd conversation memory to LangChain4j AI Services: message and token windows, per-user memory with @MemoryId, persistent stores, and why unbounded memory breaks in production.
- LangChain4j Agents & ToolsBuild tool-using agents in LangChain4j: the @Tool annotation, how the agent loop works, bounding iterations, safe write tools and the ReAct pattern — with production-ready code.
- LangChain4j Retrievers & RAGBuild RAG in LangChain4j with ContentRetriever: attach retrieval to AI Services, transform queries, re-rank results, and assemble an advanced RAG pipeline with the RetrievalAugmentor.
- LangChain4j Embedding StoresStore and search vectors in LangChain4j: the in-memory store for tests, PgVector for production, Redis and Elasticsearch, plus metadata filtering and picking the right store.