プロバイダーネイティブ構造化出力

デフォルトでは、.entity(…​) は JSON スキーマをテキスト指示としてプロンプトに追加します。これはレスポンス側のアプローチであり、モデルに指示に従うよう求め、その後レスポンスを解析します。

The complementary approach is a request-side constraint: tell the model’s provider, at the API level, that the response must conform to a schema. Most modern providers support this (OpenAI’s Structured Outputs, Anthropic’s structured output extension, Gemini’s responseSchema, Mistral’s response_format).

Spring AI exposes it portably with a switch on the EntityParamSpec consumer:

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

What changes at the wire level:

  • The system prompt no longer carries a JSON format instruction (cleaner, fewer tokens).

  • The schema is sent to the provider as an API-level field.

  • The provider’s runtime enforces conformance — invalid responses cannot be emitted at all.

This provides:

  • より高い信頼性 : the model guarantees output conforming to the schema.

  • クリーナープロンプト : no need to append format instructions.

  • パフォーマンスの向上 : models can optimize for structured output internally.

How Spring AI Detects Support

Spring AI detects native support by checking whether the model’s chat options implement the StructuredOutputChatOptions interface. If not, the flag is silently ignored and the call falls back to the prompt-based default.

対応モデル

The following providers support native structured output as of Spring AI 2.0. The same .useProviderStructuredOutput() call works regardless of which is wired in:

  • OpenAI : GPT-4o and later models with JSON Schema support.

  • Anthropic : Claude 3.5 Sonnet and later models.

  • Google GenAI : Gemini 1.5 Pro and later models.

  • Mistral AI : Mistral Small and later models with JSON Schema support.

  • Ollama : models with JSON Schema support (model-specific; see 既知の制限 ).

Why It’s Off by Default

Compatibility. Older or non-supporting models would reject the request, and the prompt-based default works everywhere.

Native structured output is not enabled by default because support varies significantly across models and providers. Enable it only when you need the stronger API-level schema enforcement it provides, and always test with your specific model version.

既知の制限

Even on providers that advertise the feature, native structured output support is often partial — the accepted JSON Schema surface varies. $ref, deeply nested arrays, allOf/anyOf/oneOf, regex patterns, and recursive types are common limitations. The shape drift this can cause is exactly what validateSchema() is good at catching.

Ollama: モデル固有の不安定性

すべての Ollama モデルが構造化出力スキーマ制約を確実に遵守するとは限りません。特に、推論モードまたは「思考」モードを組み込んだモデル (たとえば、qwen3:8bqwen3.5:9b、その他の新しい Qwen バリアント) は、内部推論トレースを構造化 JSON ではなくプレーンテキストとして返す場合があり、BeanOutputConverter で次のような逆直列化エラーが発生します。

StreamReadException: Unrecognized token 'The': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')

Ollama でこの問題が発生した場合は、別のモデル(たとえば llama3.1:latest)を試すか、デフォルトのプロンプトベースのアプローチに戻してください。また、useProviderStructuredOutput() と validateSchema() を組み合わせることで、不正なレスポンスを自動的に再試行することもできます。

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

OpenAI: トップレベル配列はサポートされていません

OpenAI 構造化出力 API は、レスポンススキーマとしてトップレベルの JSON 配列を受け付けません(OpenAI コミュニティディスカッション (英語) を参照)。ネイティブ構造化出力を有効にした List<T> をリクエストすると、API エラーが発生します。

// Does NOT work with OpenAI native structured output:
List<ActorsFilms> films = chatClient.prompt()
    .user("Generate filmographies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {},
            spec -> spec.useProviderStructuredOutput()); // fails with OpenAI

代わりに以下のいずれかの代替手段を使用してください。

// Option 1: wrap the list in a container record
record FilmographyList(List<ActorsFilms> films) {}

FilmographyList result = chatClient.prompt()
    .user("Generate filmographies for Tom Hanks and Bill Murray.")
    .call()
    .entity(FilmographyList.class, spec -> spec.useProviderStructuredOutput());
List<ActorsFilms> films = result.films();

// Option 2: use the default prompt-based approach (no native output required)
List<ActorsFilms> films = chatClient.prompt()
    .user("Generate filmographies for Tom Hanks and Bill Murray.")
    .call()
    .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

The default prompt-based flow has no such restriction — top-level arrays work fine without useProviderStructuredOutput().

Enabling Globally

useProviderStructuredOutput() is a per-call switch. To enable native structured output for every call on a ChatClient, set the AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT advisor parameter — as a default on the builder, or per request:

// Per request
ActorsFilms films = chatClient.prompt()
    .advisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class);

// Globally on the builder
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder
        .defaultAdvisors(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT)
        .build();
}

Provider Built-in JSON Mode

Independently of useProviderStructuredOutput(), some AI models expose dedicated configuration options to generate structured (usually JSON) output directly:

  • OpenAI 構造化出力 can ensure the model generates responses conforming strictly to your provided JSON Schema. Choose between JSON_OBJECT (valid JSON) or JSON_SCHEMA with a supplied schema (spring.ai.openai.chat.response-format option).

  • Ollama provides a spring.ai.ollama.chat.format option to specify the response format. Currently, the only accepted value is json.

  • Mistral AI provides a spring.ai.mistralai.chat.response-format option. Setting it to { "type": "json_object" } enables JSON mode; setting it to { "type": "json_schema" } with a supplied schema enables native structured output that matches your schema.