出力コンバーター

The high-level .entity(…​) API is built on top of the StructuredOutputConverter abstraction. Most applications never touch it directly. Reach for this lower-level API when you need to:

  • 組み込みコンバーターが拒否する出力を解析します(たとえば、マークダウンコードフェンスで囲まれた JSON など)。

  • produce a non-JSON format such as YAML or CSV;

  • use a converter directly against the low-level ChatModel API.

Spring AI Structured Output Converters convert the LLM output into a structured format. As shown in the following diagram, this approach operates around the LLM text completion endpoint:

Structured Output Converter Architecture

The structured output converter plays a role before and after the LLM call. Before the call, the converter appends format instructions to the prompt, guiding the model to generate the desired output structure. After the call, the converter parses the model’s text output and maps it into instances of the structured type.

The StructuredOutputConverter is a best effort to convert the model output into a structured output. The AI Model is not guaranteed to return the structured output as requested. Consider combining it with schema validation to ensure the model output is as expected.
StructuredOutputConverter は LLM ツール呼び出しでは使用されません。この機能は本質的にデフォルトで構造化された出力を提供するためです。

構造化出力 API

StructuredOutputConverter インターフェースを使用すると、出力を Java クラスにマッピングしたり、テキストベースの AI モデル出力から値の配列をマッピングしたりするなど、構造化された出力を取得できます。インターフェース定義は次のとおりです。

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {

    /**
     * Returns the JSON schema for the structured output of an LLM call,
     * or NO_JSON_SCHEMA ("") if not available.
     */
    default String getJsonSchema() {
        return NO_JSON_SCHEMA;
    }

}

It combines the Spring コンバーター < 文字列、T> (Javadoc) interface and the FormatProvider interface:

public interface FormatProvider {
    String getFormat();
}

次の図は、構造化出力 API を使用する場合のデータフローを示しています。

Structured Output API

FormatProvider は AI モデルに特定の書式設定ガイドラインを提供し、Converter を使用して指定されたターゲット型 T に変換できるテキスト出力を生成できるようにします。次に、このような書式設定指示の例を示します。

  Your response should be in JSON format.
  The data structure for the JSON should match this Java class: java.util.HashMap
  Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.

フォーマット指示は、ほとんどの場合、次のように PromptTemplate を使用してユーザー入力の末尾に追加されます。

    StructuredOutputConverter outputConverter = ...
    String userInputTemplate = """
        ... user text input ....
        {format}
        """; // user input with a "format" placeholder.
    Prompt prompt = new Prompt(
            PromptTemplate.builder()
                    .template(this.userInputTemplate)
                    .variables(Map.of(..., "format", this.outputConverter.getFormat())) // replace the "format" placeholder with the converter's format.
                    .build().createMessage()
    );

The Converter<String, T> is responsible for transforming output text from the model into instances of the specified type T.

getJsonSchema() のロール

Added in 2.0 as a default method on StructuredOutputConvertergetJsonSchema() is the bridge that lets a converter participate in useProviderStructuredOutput() and validateSchema() . Implement it to return your schema (typically by delegating to a BeanOutputConverter) and both switches work; leave it at the default and both switches become no-ops for that converter.

利用可能なコンバーター

Spring AI provides AbstractConversionServiceOutputConverterAbstractMessageOutputConverterBeanOutputConverterMapOutputConverterListOutputConverter implementations:

Structured Output Class Hierarchy
  • AbstractConversionServiceOutputConverter<T> - LLM 出力を目的の形式に変換するための事前構成済みの GenericConversionService (Javadoc) を提供します。デフォルトの FormatProvider 実装は提供されていません。

  • AbstractMessageOutputConverter<T> - LLM 出力を目的の形式に変換するための事前構成済みの MessageConverter (Javadoc) を提供します。デフォルトの FormatProvider 実装は提供されていません。

  • BeanOutputConverter<T> - Configured with a designated Java class (e.g., Bean) or a ParameterizedTypeReference (Javadoc) , this converter employs a FormatProvider implementation that directs the AI Model to produce a JSON response compliant with a DRAFT_2020_12 JSON Schema derived from the specified Java class. Subsequently, it utilizes a JsonMapper to deserialize the JSON output into a Java object instance of the target class.

  • MapOutputConverter - AI モデルが RFC8259 準拠の JSON レスポンスを生成するようにガイドする FormatProvider 実装により、AbstractMessageOutputConverter の機能が拡張されます。さらに、提供されている MessageConverter を使用して JSON ペイロードを java.util.Map<String, Object> インスタンスに変換するコンバーター実装も組み込まれています。

  • ListOutputConverter - AbstractConversionServiceOutputConverter を拡張し、カンマ区切りリスト出力用にカスタマイズされた FormatProvider 実装が含まれます。コンバーター実装は、提供された ConversionService を使用して、モデルテキスト出力を java.util.List に変換します。

コンバーターの使用

The following sections show how to use the available converters to generate structured outputs. Each is shown both with the high-level ChatClient API and the low-level ChatModel API.

Bean 出力コンバーター

次の例は、BeanOutputConverter を使用して俳優のフィルモグラフィーを生成する方法を示しています。

The target record representing the actor’s filmography:

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

Here is how to apply the BeanOutputConverter using the high-level, fluent ChatClient API:

ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
                    .param("actor", "Tom Hanks"))
        .call()
        .entity(ActorsFilms.class);

または、低レベルの ChatModel API を直接使用します。

BeanOutputConverter<ActorsFilms> beanOutputConverter =
    new BeanOutputConverter<>(ActorsFilms.class);

String format = this.beanOutputConverter.getFormat();

String actor = "Tom Hanks";

String template = """
        Generate the filmography of 5 movies for {actor}.
        {format}
        """;

Generation generation = chatModel.call(
    PromptTemplate.builder().template(this.template).variables(Map.of("actor", this.actor, "format", this.format)).build().create()).getResult();

ActorsFilms actorsFilms = this.beanOutputConverter.convert(this.generation.getOutput().getText());

生成されたスキーマ内のプロパティの順序

BeanOutputConverter は、@JsonPropertyOrder アノテーションを通じて、生成された JSON スキーマ内のカスタムプロパティの順序付けをサポートします。このアノテーションを使用すると、クラスまたはレコード内の宣言順序に関係なく、スキーマ内でプロパティが表示される正確な順序を指定できます。

例: ActorsFilms レコード内のプロパティの特定の順序を確保するには:

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

このアノテーションは、レコードと通常の Java クラスの両方で機能します。

汎用 Bean 型

より複雑なターゲットクラス構造を指定するには、ParameterizedTypeReference コンストラクターを使用します。例: 俳優とそのフィルモグラフィーのリストを表すには、次のようにします。

List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
        .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
        .call()
        .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

または、低レベルの ChatModel API を直接使用します。

BeanOutputConverter<List<ActorsFilms>> outputConverter = new BeanOutputConverter<>(
        new ParameterizedTypeReference<List<ActorsFilms>>() { });

String format = this.outputConverter.getFormat();
String template = """
        Generate the filmography of 5 movies for Tom Hanks and Bill Murray.
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template).variables(Map.of("format", this.format)).build().create();

Generation generation = chatModel.call(this.prompt).getResult();

List<ActorsFilms> actorsFilms = this.outputConverter.convert(this.generation.getOutput().getText());

マップ出力コンバーター

次のスニペットは、MapOutputConverter を使用してモデル出力をマップ内の数値のリストに変換する方法を示しています。

Map<String, Object> result = ChatClient.create(chatModel).prompt()
        .user(u -> u.text("Provide me a List of {subject}")
                    .param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
        .call()
        .entity(new ParameterizedTypeReference<Map<String, Object>>() {});

または、低レベルの ChatModel API を直接使用します。

MapOutputConverter mapOutputConverter = new MapOutputConverter();

String format = this.mapOutputConverter.getFormat();
String template = """
        Provide me a List of {subject}
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template)
.variables(Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", this.format)).build().create();

Generation generation = chatModel.call(this.prompt).getResult();

Map<String, Object> result = this.mapOutputConverter.convert(this.generation.getOutput().getText());

リスト出力コンバーター

次のスニペットは、ListOutputConverter を使用してモデル出力をアイスクリームのフレーバーのリストに変換する方法を示しています。

List<String> flavors = ChatClient.create(chatModel).prompt()
                .user(u -> u.text("List five {subject}")
                            .param("subject", "ice cream flavors"))
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));

または、低レベルの ChatModel API を直接使用します。

ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());

String format = this.listOutputConverter.getFormat();
String template = """
        List five {subject}
        {format}
        """;

Prompt prompt = PromptTemplate.builder().template(this.template).variables(Map.of("subject", "ice cream flavors", "format", this.format)).build().create();

Generation generation = this.chatModel.call(this.prompt).getResult();

List<String> list = this.listOutputConverter.convert(this.generation.getOutput().getText());

カスタムコンバーター

The built-in BeanOutputConverter is strict: it expects the model’s response to be parseable JSON, full stop. But models often wrap their JSON in markdown code fences:

Here's the filmography:
```json
{ "actor": "Tom Hanks", "movies": ["Forrest Gump", "Cast Away"] }
```

BeanOutputConverter will throw on the first H of "Here’s". The common fix is a custom converter that strips fences and extracts the JSON before delegating to the default parser:

public class LenientJsonOutputConverter<T> implements StructuredOutputConverter<T> {

    private static final Pattern FENCE = Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```");

    private final BeanOutputConverter<T> delegate;

    public LenientJsonOutputConverter(Class<T> targetType) {
        this.delegate = new BeanOutputConverter<>(targetType);
    }

    @Override public String getFormat()     { return delegate.getFormat(); }
    @Override public String getJsonSchema() { return delegate.getJsonSchema(); }

    @Override
    public T convert(String source) {
        var matcher = FENCE.matcher(source);
        String json = matcher.find() ? matcher.group(1).trim() : source.trim();
        return delegate.convert(json);
    }
}

Pass it to .entity(…​) instead of a Class:

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

Because this converter delegates getJsonSchema() to the underlying BeanOutputConverter, both reliability switches still work — validateSchema() and useProviderStructuredOutput() operate against the same schema the default converter would use. See The Role of getJsonSchema()

Non-JSON Formats

For formats outside JSON’s reach — YAML for config generators, CSV for data extraction — implement StructuredOutputConverter from scratch: write your own getFormat() prompt and your own convert(…​) parser. Leave getJsonSchema() at its default, and both reliability switches sit out — the prompt-based path runs as it does for the built-ins.