skip to content

How does BeanOutputConverter turn a free-text LLM reply into a typed Java POJO?

level: seniorimportance: must knowfreq 68%

answer

  1. getFormat() = JSON schema instruction into prompt
  2. convert() = Jackson JSON -> POJO
  3. StructuredOutputConverter = FormatProvider + Converter<String,T>
  4. ChatClient.entity(Class) is the sugar
  5. no guarantee -> convert can throw

basics

~10 s

BeanOutputConverter<T> generates a JSON-schema format instruction from the target type via getFormat(); you append that to the prompt so the model replies in JSON, then convert(reply) deserializes that JSON into your POJO with Jackson.

solid answer

~40 s

`BeanOutputConverter<T>` (in `org.springframework.ai.converter`) implements `StructuredOutputConverter`, which combines `FormatProvider` and `Converter<String,T>`. It works in two halves. First, `getFormat()` derives a JSON Schema from your target class (or a `ParameterizedTypeReference` for generics) and returns instruction text telling the model to respond as JSON matching that schema, no markdown fences. You append this to your prompt. Second, after the model returns a String, `convert(text)` parses that JSON into an instance of `T` using a Jackson `ObjectMapper` — stripping stray ```json fences it finds. The high-level `ChatClient` hides all this: `chatClient.prompt().user(q).call().entity(MyDto.class)` builds a BeanOutputConverter, injects the format, and deserializes for you. The key caveat: this is prompt-engineered, not guaranteed — a non-conforming reply throws during convert, so you handle parse failures.

code

java · 21 lines
java
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.model.ChatModel;
import java.util.Map;

public class AuthorExtractor {
    record Author(String name, java.util.List<String> books) {}

    private final ChatModel chatModel;
    AuthorExtractor(ChatModel chatModel) { this.chatModel = chatModel; }

    Author extract(String authorName) {
        BeanOutputConverter<Author> converter = new BeanOutputConverter<>(Author.class);
        Prompt prompt = new PromptTemplate(
                "List the author {name} and their books.\n{format}")
            .create(Map.of("name", authorName, "format", converter.getFormat()));
        String reply = chatModel.call(prompt).getResult().getOutput().getText();
        return converter.convert(reply); // JSON -> Author via Jackson
    }
}

go deeper

for a junior

Knows it maps a JSON reply to a Java object.

for a middle

Can wire getFormat() into the prompt and call convert(), or use ChatClient.entity().

for a senior

Explains the two-phase FormatProvider+Converter design, generics via ParameterizedTypeReference, and failure handling.

for a principal

Weighs converter-based prompting vs provider-native structured output, retry/validation strategy, and schema-annotation governance for reliability at scale.

**Problem it solves.** LLMs return free text, but applications want typed objects. `BeanOutputConverter<T>` bridges that gap by (a) instructing the model to emit structured JSON and (b) deserializing that JSON into your class. **The interface hierarchy.** `StructuredOutputConverter<T>` extends both: - `FormatProvider` → `String getFormat()`: the instruction text appended to the prompt. - `org.springframework.core.convert.converter.Converter<String,T>` → `T convert(String)`: parses the model's reply. Implementations: `BeanOutputConverter<T>` (arbitrary POJO), `ListOutputConverter` (List<String>), `MapOutputConverter` (Map<String,Object>). (These were renamed from the older `*OutputParser` classes.) **Half 1 — getFormat().** BeanOutputConverter builds a JSON Schema from the target type's fields (using Jackson + a schema generator) and returns text roughly like: *"Your response should be in JSON format. Do not include markdown code blocks. Here is the JSON Schema instance: { ... }"*. You concatenate this onto your user/system prompt so the model knows the exact shape. **Half 2 — convert().** When the reply comes back as a String, `convert()` runs it through a Jackson `ObjectMapper`. Modern versions defensively strip leading/trailing ```json … ``` fences before parsing. The result is a populated instance of `T`. **Manual wiring.** ```java BeanOutputConverter<Author> conv = new BeanOutputConverter<>(Author.class); String format = conv.getFormat(); Prompt prompt = new PromptTemplate("Generate an author bio for {name}.\n{format}") .create(Map.of("name", "Ada", "format", format)); String reply = chatModel.call(prompt).getResult().getOutput().getText(); Author author = conv.convert(reply); ``` **High-level ChatClient equivalent.** `chatClient.prompt().user("Generate a bio for Ada").call().entity(Author.class)` does all of the above internally — creating the converter, appending format, and converting. **Generics / collections.** For a `List<Author>` (list of beans, not strings), do NOT use ListOutputConverter (that's List<String>). Instead use `new BeanOutputConverter<>(new ParameterizedTypeReference<List<Author>>() {})`, or `.entity(new ParameterizedTypeReference<List<Author>>() {})` on ChatClient. **Edge cases & gotchas.** - **No hard guarantee.** The model may ignore the format or add prose; `convert()` then throws (Jackson parse error). Wrap in error handling / retry (Spring Retry, or a `RetryAdvisor`). - **Markdown fences.** Older versions choked on ```json wrappers; newer ones strip them, but relying on a specific version matters. - **Extra fields / hallucinated keys.** Configure ObjectMapper leniency; by default unknown properties may fail unless ignored. - **Descriptions help.** Annotate fields with `@JsonPropertyDescription` / `@JsonProperty` to steer the schema and improve model accuracy. - **Native structured output is separate.** Some providers support a real response-format/JSON-mode constraint; the converter is provider-agnostic prompt engineering and doesn't replace that. - **Temperature.** Lower temperature improves format adherence. **When to use.** Use BeanOutputConverter (or ChatClient.entity) whenever you need the model's answer as a domain object — extraction, classification into a structured record, form-filling. Prefer the ChatClient `.entity()` sugar unless you need manual control over prompt assembly.

  • How do you deserialize a List<Author> rather than a single Author?
    Use a ParameterizedTypeReference to preserve the generic type: new BeanOutputConverter<>(new ParameterizedTypeReference<List<Author>>(){}), or ChatClient .entity(new ParameterizedTypeReference<List<Author>>(){}). ListOutputConverter won't work — it only produces List<String>.
  • What happens if the model returns prose instead of valid JSON, and how do you harden this?
    convert() throws a Jackson parse exception. Harden with lower temperature, clear/strong format instructions, retry logic (Spring Retry or a retry advisor), provider native JSON mode where available, and field @JsonPropertyDescription hints to steer output.

saying these in an interview costs you the question

  • Claiming BeanOutputConverter guarantees valid structured output
  • Using ListOutputConverter for a list of POJOs
  • Forgetting to append getFormat() to the prompt
  • Thinking convert() calls the model (it only parses the returned string)
  • Losing the generic type by using Class instead of ParameterizedTypeReference for collections

context