惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
Jina AI
Jina AI
B
Blog
Recorded Future
Recorded Future
MyScale Blog
MyScale Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
爱范儿
爱范儿
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
博客园 - Franky
Schneier on Security
Schneier on Security
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
H
Hacker News: Front Page
Cloudbric
Cloudbric
D
DataBreaches.Net
B
Blog RSS Feed
P
Palo Alto Networks Blog
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
I
Intezer
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
F
Fortinet All Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
K
Kaspersky official blog
Forbes - Security
Forbes - Security

Hacker News - Newest: "AI"

AI can't read an investor deck AI as an attorney? Student uses ChatGPT, Gemini to sue UW over alleged racial discrimination Hacking MCP Servers in AI Systems – The Rug Pull: Tool Changes After Approval GitHub - MeepCastana/KubeezCut: Free Web based video editor GitHub - GenAI-Gurus/awesome-eu-ai-act: Curated tools, official sources, OSS, templates, and guides for EU AI Act compliance. Can AI judge journalism? A Thiel-backed startup says yes, even if it risks chilling whistleblowers Coming soon: 10 Things That Matter in AI Right Now DARPA built an AI to fact-check enemy weapons claims What explains heterogeneity in AI adoption? When AI Meets Muscle: Context-Aware Electrical Stimulation Promises a New Way to Guide Human Movements - Department of Computer Science AI Changed How We Build. It Did Not Change What Matters. Linux rules on using AI-generated code - Copilot is OK, but humans must take 'full responsibility for the… Meta spins up AI version of Mark Zuckerberg to engage with employees Code Mode: Let Your AI Write Programs, Not Just Call Tools | TanStack Blog GitHub - Delavalom/graft: Go framework for building AI agents. Type-safe tools, multi-provider (OpenAI, Anthropic, Gemini, Bedrock), zero vendor SDKs. India's TCS tops estimates, says new AI models did not dent services demand Gen Z's fading AI hype Strong feeling: we are in a folded AI reality GitHub - machinarii/total-recall-catalog: A reference catalog of latest knowledge retrieval, memory & RAG systems GitHub - mensfeld/code-on-incus: Give each AI agent its own isolated machine with root, Docker, and systemd. Active defense detects and stops threats automatically.. Quantization, LoRA, and the 8% Problem: Benchmarking Local LLMs for Production AI Iran war: We spoke to the man making Lego-style AI videos that experts say are powerful propaganda Powell, Bessent discussed Anthropic's Mythos AI cyber threat with major U.S. banks GitHub - immartian/bellamem: Persistent belief-graph memory for AI agents. Retrieves decisive context by importance — not recency, not RAG, not /compact. recursive-mode: The Repo-Native Operating System for AI Engineering After the attack on Sam Altman's home, will AI CEO's go on the offensive? The biggest advance in AI since the LLM Opus 4.6 vs GPT 5.4 One Prompt Unity World Generation Test “AI polls” are fake polls Client Challenge Can AI be a 'child of God'? Inside Anthropic's meeting with Christian leaders How to Switch AI Chatbots and Why You Might Want To GitHub - MattMessinger1/agentic_refund_guardrail: Safe refund policy layer for AI agents — Python + TypeScript. Same behavior, shared tests. Adam/papers/emergent_values_whitepaper.md at master · strangeadvancedmarketing/Adam Ask HN: How do you stop playing 20 questions with your AI coding tools How far can automation and AI support psychotherapy? - @theU GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits A Mac Studio for Local AI — 6 Months Later A History of the Early Years of AI at the University of Edinburgh Why AI Coding Tools Still Feel Stuck on Localhost MSN AI Datacenters Are Becoming Strategic Targets twitter.com Penn Researchers Use AI to Surface Unreported GLP-1 Side Effects in Reddit Posts Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 AI models are terrible at betting on soccer—especially xAI Grok GitHub - xialeistudio/echoic GitHub - HimashaHerath/github-dev-wrapped: AI-powered weekly GitHub activity reports deployed to GitHub Pages GitHub - alejandrobalderas/claude-code-from-source: Architecture, patterns & internals of Anthropic's AI coding agent — reverse-engineered from source maps AI and Tech brief: Ireland ascendant GitHub - Titovilal/context0: Context0 - Never Surrender Training for a Marathon with an AI Coach: What Worked and What Didn't Cyber Pulse: Agentic Intel - Apps on Google Play I Built an AI PR Reviewer That Catches Bugs by Not Looking for Bugs Gen Z workers are so fearful AI will take their job they’re intentionally sabotaging their company’s AI rollout | Fortune How AI Is Reimagining the Game of Golf–For Both Players and Courses GitHub - nattergabriel/reseed: A CLI tool for managing and distributing agent skills across projects Is SVG the final frontier? My AI workflow evolved from prompts to a near-autonomous workflow MLSharp Help - 3DGS Viewer & Generator I put my cognitive field based AI's runtime on GitHub Is Numble the first AI-proof game? A3: Kubernetes for autonomous AI agent fleets | Emergent Principles Deepali Vyas ("The Elite Recruiter") GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Unionized ProPublica staff are on strike over AI, layoffs, and wages Unleashing the Advantage of Quantum AI We're heading for an AI-fueled 'dementia crisis,' brain scientist warns The AI-Assisted Breach of Mexico's Government Infrastructure [pdf] GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. MSN GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs AI Code is Hollowing Out Open Source, and Maintainers are Looking the Other Way What leaked "SteamGPT" files could mean for the PC gaming platform's use of AI AI is the boss at this retail store. What could go wrong? GitHub - Wuzu11517/agentic-proxy: Local proxy meant to help reduce With Drones, Geophysics and ArtificiaI Intelligence, Researchers Prepare to Do Battle Against Land Mines A Single Operator, Two AI Platforms, Nine Government Agencies: The Full Technical Report 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - inevolin/resume-cli: Hit Claude usage limits? Resume any AI coding session elsewhere. Switch tools at zero friction. GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. How to Build a Secure AI PR Reviewer with Claude, GitHub Actions, and JavaScript This Startup Wants You to Pay Up to Talk With AI Versions of Human Experts Intel Arc Pro B70 Brings 32GB VRAM to Local AI for $949 WordPress 7.0: The Good, the AI, and the Still Missing AI on the couch: Anthropic gives Claude 20 hours of psychiatry IatroBench: Pre-Registered Evidence of Iatrogenic Harm from AI Safety Measures AI Agents Know About Supabase. They Don't Always Use It Right. The history and future of AI at Google, with Sundar Pichai Inside an AI‑enabled device code phishing campaign How Meta Used AI to Map Tribal Knowledge in Large-Scale Data Pipelines AI for Systems: Using LLMs to Optimize Database Query Execution Forecasting the Economic Effects of AI Introducing Tinker: Play with AI, bring your ideas to life AI sheds light on an ancient gaming mystery People really hate AI but not as much as Iran—or Democrats | Fortune What is an AI Product Engineer? Phoebe Gates wants her $185 million AI startup to succeed with 'no ties to my privilege or my last name': 'I have a chip on my shoulder' | Fortune
Self-Correcting Structured Output in Spring AI 2.0
tzolov · 2026-06-23 · via Hacker News - Newest: "AI"

Large language models are text-in, text-out systems — their interface is natural language.

Natural language is a great interface for humans and a poor one for software. The moment downstream code needs to route on a field, persist a value, or branch on a result, the conversation has to become a record. Structured output bridges that gap. The model is steered to produce text conforming to a schema; the application parses it back into a typed object the rest of the codebase can treat like any other domain type.

Spring AI has supported structured output since day one through ChatClient.call().entity(...). Spring AI 2.0 adds two new dials to it — provider-native structured output and self-correcting schema validation. Defaults are unchanged, so existing code keeps working.

This post walks through structured output, starting with a simple, working case first, then add reliability piece by piece.


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);

That's it. The result is a typed ActorsFilms you can pass to the rest of your code:

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

.entity(...) is .call()-only. Typed parsing requires the complete response, so it isn't available on the streaming path (.stream() returns text chunks, not typed objects). This applies to every variant covered below — Class, ParameterizedTypeReference, custom converter, with or without the new dials.

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 then handed to a type converter that parsed it back into your record.

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

It also has no guarantees. The model is asked to produce JSON matching the schema, not forced to. Most of the time it complies. Sometimes it doesn't — it returns an extra field, omits a required one, or wraps the JSON in prose. When that happens, the parser throws.

The next two sections fix that, one approach at a time.


Adding a Safety Net: validateSchema()

The simplest way to handle malformed output is to detect it and retry. Spring AI 2.0 does this automatically with a single switch:

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

The spec -> spec.validateSchema() consumer turns on a self-correcting retry loop:

  1. The model responds.
  2. Spring AI validates the response against the schema for ActorsFilms.
  3. If validation passes, you get your typed record back.
  4. If it fails, the validation error ("missing required field actor", "expected array, got string") is appended to the user prompt, and the call is re-issued — up to 3 attempts by default.

The model sees the specific error on each retry. The second attempt isn't a blind re-try; the model knows what was wrong and can correct it.

This is powered by StructuredOutputValidationAdvisor, a recursive advisor that's auto-registered when you call validateSchema(). You don't have to wire anything; the switch is the entire configuration.

Customizing the advisor

StructuredOutputValidationAdvisor defaults to 3 retry attempts and uses Spring AI's default JsonMapper. To customize — for example, more attempts, a pre-supplied schema, or a different mapper — build your own instance and register it on the ChatClient. An explicitly registered advisor replaces the auto-registered one:

var validationAdvisor = StructuredOutputValidationAdvisor.builder()
    .outputType(ActorsFilms.class)
    .maxRepeatAttempts(5)
    .build();

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(validationAdvisor)
    .build();

Adding Upstream Guarantees: useProviderStructuredOutput()

validateSchema() is a response-side safety net — it catches bad output after the fact and retries. 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 another switch on the same 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 can't be emitted at all.

Supported providers as of 2.0: OpenAI, Anthropic, Google GenAI , Mistral AI, Ollama (model-specific). The same .useProviderStructuredOutput() call works regardless of which is wired in.

Spring AI detects 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.

Why it's off by default

Compatibility. Older or non-supporting models would reject the request, and the prompt-based default works everywhere. A few known limitations are worth mentioning even on supported providers:

  • Partial JSON Schema support. Native structured output support is often partial — even on providers that advertise the feature, 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() (next section) is good at catching.
  • Ollama with reasoning ("thinking") models — variants like qwen may emit their internal reasoning trace as plain text instead of JSON, causing parse errors. Use a non-reasoning model, or combine native output with validateSchema() so misbehaving responses are retried.
  • OpenAI doesn't accept top-level arrays in its Structured Outputs API. Wrap a list in a container record before requesting it natively (record FilmographyList(List<ActorsFilms> films) {}).

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, the Ollama reasoning quirk above — and corrects them automatically.

Reach for both when downstream code can't tolerate shape drift — when a missing field or wrong type would corrupt state, throw later, or silently misroute.


Generic Types: 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>>() {});

The same EntityParamSpec consumer works:

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

One thing to watch out for: OpenAI's Structured Outputs API doesn't accept a top-level array. If you combine List<...> with .useProviderStructuredOutput() on OpenAI, the call will fail. The fix is a one-line wrapper record:

record FilmographyList(List<ActorsFilms> films) {}

FilmographyList result = chatClient.prompt()
    .user("Generate filmographies for three random actors.")
    .call()
    .entity(FilmographyList.class, spec -> spec.useProviderStructuredOutput());

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


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(...)Class, ParameterizedTypeReference, and the EntityParamSpec consumer all apply.


When the Built-ins Aren't Enough

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 new dials still work — validateSchema() and useProviderStructuredOutput() operate against the same schema the default converter would use. You get resilient parsing plus self-correction with no extra wiring.

The role of getJsonSchema(). Added in 2.0 as a default method on StructuredOutputConverter, getJsonSchema() 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 the new dials work; leave it at the default and both dials become no-ops for that converter.

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 new dials sit out — the prompt-based path runs as it does for the built-ins.


Cheat Sheet

You need Use
Default — 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())
Both — 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 Not supported — .entity(...) is .call()-only; .stream() returns text chunks, not typed objects

Wrapping Up

Structured output in Spring AI 2.0 is the same .entity(...) call you already know, with two new switches: validateSchema() for response-side self-correction, useProviderStructuredOutput() for request-side enforcement. Each is independently useful; together they constrain the request and self-correct the response. Existing code keeps working unchanged; new code can opt in per call without rewiring the application.


References