構造化された出力

大規模な言語モデルは、テキスト入力・テキスト出力のシステムです。下流のコードがフィールドに基づいてルーティングしたり、値を永続化したり、結果に対してブランチを実行したりする必要が生じた瞬間、そのテキストは型付きレコードに変換されなければなりません。Structured output はこのギャップを埋めます。モデルはスキーマに準拠したテキストを生成するように制御され、アプリケーションはそれを解析して、コードベースの残りの部分が他のドメイン型と同様に扱うことができる型付きオブジェクトに変換します。

Spring AI exposes structured output directly on the ChatClient fluent API through .entity(…​). You define a Java type for the shape you want back, and Spring AI takes care of the rest: a JSON schema is generated from your type, the model is instructed to honor it, and the response is deserialized into your type.

This page covers the high-level ChatClient path. For reliability switches and lower-level API, see:

Typed Response

Define a Java record for the shape you want back:

record ActorsFilms(String actor, List<String> movies) {}

Ask ChatClient to populate it. Instead of finishing the call with .content() — which returns the raw text reply — finish with .entity(…​) and pass your target type:

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class);

The result is a typed ActorsFilms you can pass to the rest of your code:

films.actor();     // "Tom Hanks"
films.movies();    // ["Forrest Gump", "Cast Away", ...]

Behind the scenes, Spring AI did three things: a schema generator turned your ActorsFilms record into a JSON schema, that schema was appended to the prompt’s system context, and the model’s JSON answer was handed to a type converter that parsed it back into your record.

Structured Output Basic Flow

This works on every model Spring AI supports — there is nothing provider-specific about it.

.entity(…​) is .call() -only. Typed parsing requires the complete response, so it is not available on the streaming path (.stream() returns text chunks, not typed objects). This applies to every variant on this page — ClassParameterizedTypeReference, custom converter, with or without the reliability switches.

The default .entity(…​) call has no guarantees. The model is asked to produce JSON matching the schema, not forced to. Most of the time it complies; sometimes it returns an extra field, omits a required one, or wraps the JSON in prose, and the parser throws. The two switches below address this.

総称型: Lists, Maps, and Beyond

.entity(Class) is for concrete classes. For generic types — List<ActorsFilms>Map<String, ActorsFilms> — use ParameterizedTypeReference:

List<ActorsFilms> films = chatClient.prompt()
    .user("Generate filmographies for three random actors.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

Reliability Switches: EntityParamSpec

All .entity(…​) (and .responseEntity(…​)) overloads accept an optional Consumer<EntityParamSpec> that enables two independent, composable behaviors.

Don’t Fail on Malformed Output: validateSchema()

validateSchema() turns on a self-correcting retry loop: Spring AI validates the response against the entity’s schema, and if validation fails, the specific error is appended to the prompt and the call is re-issued — up to 3 attempts by default.

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class, spec -> spec.validateSchema());

See スキーマ検証と自己修正 for how the retry loop works and how to customize it.

Stronger Upstream Guarantees: useProviderStructuredOutput()

useProviderStructuredOutput() sends the schema to the provider as an API-level constraint, so the provider’s runtime enforces conformance rather than relying on prompt instructions.

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class, spec -> spec.useProviderStructuredOutput());

This requires the underlying model to support native structured output. See プロバイダーネイティブ構造化出力 for supported providers and limitations.

Combining Both

The two switches solve different problems and compose naturally:

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class, spec -> spec
        .useProviderStructuredOutput()
        .validateSchema());

useProviderStructuredOutput() minimizes the chance of malformed output by constraining the model at the API level. validateSchema() catches the residual cases — provider edge cases, reasoning-model quirks — and corrects them automatically. Reach for both when downstream code cannot tolerate shape drift.

Getting the Full Response

.entity(…​) returns only the parsed object. If you also need the underlying ChatResponse — for token usage, observability metadata, or anything beyond the entity — use .responseEntity(…​):

ResponseEntity<ChatResponse, ActorsFilms> result = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .responseEntity(ActorsFilms.class);

ActorsFilms films = result.entity();
ChatResponse raw = result.response();
long totalTokens = raw.getMetadata().getUsage().getTotalTokens();

It has the same overload set as .entity(…​) — ClassParameterizedTypeReference, custom StructuredOutputConverter, and the EntityParamSpec consumer all apply.

Custom and Non-JSON Output

When the built-in JSON parsing is not enough — the model wraps JSON in markdown fences, or you need a non-JSON format such as YAML or CSV — pass your own StructuredOutputConverter<T> to .entity(…​). See 出力コンバーター

虎の巻

You need 使用

デフォルト — works on every provider

.entity(Type.class)

Generic types like List<T>Map<K,V>

.entity(new ParameterizedTypeReference<…​>() {})

Don’t fail on malformed output

.entity(Type.class, spec → spec.validateSchema())

Stronger upstream guarantees from the provider

.entity(Type.class, spec → spec.useProviderStructuredOutput())

両方 — request constraint + response retry

.entity(Type.class, spec → spec.useProviderStructuredOutput().validateSchema())

Token usage / metadata alongside the entity

.responseEntity(…​) (same overloads)

Model wraps JSON in markdown fences, or non-JSON format

Implement StructuredOutputConverter<T> and pass it to .entity(…​)

Streaming responses

未サポート — .entity(…​) is .call() -only; .stream() returns text chunks, not typed objects

Structured output is a best effort to convert the model output into a structured type. The model is not guaranteed to return the requested structure. Use validateSchema() and/or useProviderStructuredOutput() when correctness matters.
StructuredOutputConverter は LLM ツール呼び出しでは使用されません。この機能は本質的にデフォルトで構造化された出力を提供するためです。