Spring AI 1.x to 2.0: The Migration Guide (What Breaks, and What Breaks Silently)
A complete Spring AI 1.x to 2.0 migration guide, verified against Spring AI 2.0.0 on Spring Boot 4.1. Boot 4 is mandatory and Jackson moves to 3. internalToolExecutionEnabled and toolNames are removed outright, not renamed. Chat memory becomes stricter: 2.0 removes the remaining default-ID and builder-based configuration paths, while explicit per-request conversation IDs — introduced during the 1.x line — become the only safe migration target. Plus the changes that produce no compile error at all.
Migration guides divide breaking changes into two kinds, and only one of them deserves your attention. The first kind stops the build: a renamed method, a removed class, a moved package. Annoying, mechanical, and safe — the compiler finds every instance for you.
The second kind compiles perfectly and changes behaviour. Spring AI 2.0 has an unusual amount of the second kind — schema generation that quietly stops marking fields required, a serialization library swap that reorders your JSON, and a chat-memory tightening that removes the convenient path a lot of 1.x code was built on.
This guide covers both, verified against Spring AI 2.0.0 on Spring Boot 4.1. The companion repository asserts the subtle ones as runnable tests — and it runs with no API key, no network and no provider account, because everything that changed sits above the model.
Versions (August 2026): Verified against Spring AI 2.0.0 (the last 1.x line is 1.1.8), Spring Boot 4.1.0 and JDK 25.0.3. Note that Spring AI is not managed by the Spring Boot BOM — you import spring-ai-bom yourself and are responsible for keeping the pair compatible.
Before anything else: Boot 4 is not optional
Spring AI 2.0 is built on the Boot 4 dependency model and cannot be loaded in a Boot 3 context. There is no partial upgrade, no compatibility shim, and no version of Spring AI 2.0 that works on Boot 3.
That single fact should determine your plan:
The reason to split it is not caution for its own sake. A Boot 3 → 4 upgrade and a Spring AI 1 → 2 upgrade each produce their own crop of failures, and if you do them together you cannot tell which change caused which breakage. Spring AI 1.1.x runs on Boot 4, so the intermediate state is real and shippable.
removed — use Anthropic support, per Minimax's own guidance. Minimax embeddings are gone entirely.
Jackson 2 → 3: the change your compiler will not find
Spring AI 2.0 inherits Boot 4's move from Jackson 2 to Jackson 3 — package com.fasterxml.jackson becomes tools.jackson.
This is the single highest-risk item in the migration, and it produces almost no compile errors in application code. Jackson 3 changes the default date format and property order. If you snapshot-test JSON, persist serialized model output, cache responses as JSON, or have any downstream consumer parsing your payloads, those are all affected — and every one of them still compiles. Budget integration testing, not a find-and-replace. The direct API impact inside Spring AI is smaller than you would expect, because model options no longer depend on Jackson directly (several ModelOptionsUtils members were removed for exactly that reason); the risk is in your serialization, not theirs.
One concrete API consequence worth knowing if you use MCP: the elicit(…) overloads on McpAsyncRequestContext and McpSyncRequestContext that took tools.jackson.core.type.TypeReference<T> now take Spring's own org.springframework.core.ParameterizedTypeReference<T>.
Options: setters are gone
Every setX(…) on options classes is deprecated or removed. Builders only:
// Before
OpenAiChatOptions options = new OpenAiChatOptions();
options.setTemperature(0.7); // no longer compiles
// After
OpenAiChatOptions options = OpenAiChatOptions.builder()
.temperature(0.7)
.build();
Two renames hide in the same area, and both are easy to miss in a large codebase:
N() becomes n() in *Options builders and configuration properties.
ChatClient also no longer accepts a pre-built ChatOptions instance — use the builder lambda form.
And on the configuration side, the .options prefix is gone for every non-chat model type — embedding, image, audio, moderation, OCR:
# Before
spring.ai.openai.embedding.options.model=text-embedding-3-small
# After
spring.ai.openai.embedding.model=text-embedding-3-small
Default values are also no longer duplicated into *Properties classes. If you read a default off a properties bean, it may now be null.
Tool calling: removed, not renamed
This is the largest semantic change, and the word “removed” is doing real work.
internalToolExecutionEnabled is gone from ToolCallingChatOptions and every provider-specific options class, along with the property spring.ai.<provider>.chat.internal-tool-execution-enabled.
.internalToolExecutionEnabled(false) no longer compiles.
.internalToolExecutionEnabled(true) no longer compiles and the behaviour it named no longer exists — per-model internal tool execution has been removed from every ChatModel implementation.
If you relied on the model driving the tool-call loop internally, that loop is gone and orchestration moves up into the advisor layer. That is a design change, not a migration step, and it is the one most likely to need actual rework rather than edits.
Also removed:
streamToolCallResponses from ToolCallingAdvisor.Builder, ToolCallAdvisor.Builder and ToolSearchToolCallingAdvisor.Builder. The upgrade notes are unusually candid here: the underlying design flaw could not be fixed without a breaking change to ChatClientResponse, so the option was deleted rather than patched.
SpringBeanToolCallbackResolver, and with it the whole pattern of declaring bare Function / Supplier / Consumer beans and referencing them by name.
toolNames() from all chat options classes and from ChatClient.
Name-based tool wiring must become explicit ToolCallback instances.
Chat memory: stricter, with one path left
Four changes landed together, and their combined effect is the most important thing in this guide.
Change
Effect
ChatMemory.DEFAULT_CONVERSATION_ID removed
no implicit "default" conversation any more
Conversation ID no longer optional
must be supplied per request
.conversationId(String) removed from advisor builders
cannot be set at construction time
PromptChatMemoryAdvisor removed
use MessageChatMemoryAdvisor
The important thing to be clear about: explicit per-request conversation IDs are not new. They were introduced during the 1.x line and were already the recommended way to do this. What 2.0 does is remove the alternatives — the default-ID constant and the builder-based configuration path — so the explicit form becomes the only one available, and therefore the migration target.
That matters because the default-ID path was easy to lean on. Omitting the ID in 1.x compiled and ran, with everything landing in a single shared conversation called "default". Fine for a demo; wrong for anything multi-user, where it puts every caller's history in one bucket. Removing the fallback turns “I forgot” from silent behaviour into a compile error.
In 2.0 the ID travels as a per-request advisor param:
client.prompt()
.user("what is my name?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42"))
.call()
.content();
That shape suits how the ID is actually scoped. In a web application it is per user or per session, so it cannot sensibly be baked into a singleton ChatClient — which is exactly what .conversationId() on the builder invited you to do.
The companion repository proves the isolation rather than asserting it. Two conversations, one client:
bob's prompt: USER what did you hear?
>> Passing distinct ids keeps the conversations separate -- and in 2.0
>> there is no longer a way NOT to pass one. Under 1.x the same code
>> with the ids omitted would have compiled, put alice and bob in the
>> shared 'default' conversation, and failed this assertion silently.
Do not fix the compile error the obvious way.ChatMemory.CONVERSATION_ID still exists, so an IDE will happily offer it when DEFAULT_CONVERSATION_ID fails to resolve. They are opposites. The survivor is the metadata key you use to pass an ID; the removed one was the value"default" used when you passed none. Substituting one for the other clears the error and restores exactly the shared-conversation behaviour that removing it was meant to take away — which is the easiest way to complete the migration and keep the problem it was designed to eliminate.
The advisor swap is not cosmetic either. PromptChatMemoryAdvisor injected history into the system prompt as text; MessageChatMemoryAdvisor adds real Message objects. Providers treat those differently, so expect prompt shape and token counts to change after migration — and re-check any prompt-length budgeting.
One more, if you persist memory: messages read back from a ChatMemoryRepository (JDBC, Cassandra, MongoDB, Neo4j, Redis) now carry a creation timestamp in metadata. A retrieved message is therefore notequals() to an otherwise identical message built in code. Anything comparing messages by value, or keying a Set or Map on them, changes behaviour without any error.
Structured output: schema changes that produce no error
Three changes to BeanOutputConverter and JSON Schema generation:
Kotlin properties that are optional in their primary constructor (nullable or defaulted) are no longer in the schema's required array.
Properties annotated @JsonProperty(required = false) — including any @JsonProperty that does not state required explicitly — are no longer treated as required.
BeanOutputConverter.postProcessSchema(JsonNode) is removed; subclasses overriding it will not compile.
The first two change the schema you send to the model. The model then legitimately omits fields it previously filled in, and you get nulls where you expected values — with no exception anywhere. If you depend on a field being populated, mark it required explicitly now.
The order that works
Boot 3 → Boot 4 alone, with Spring AI still on 1.1.x. Ship it.
Bump the Spring AI BOM to 2.0.0 and clear the compile errors: options builders, toolNames(), internalToolExecutionEnabled, module renames.
Fix chat memory. Every advisor call site needs a conversation ID. Grep for DEFAULT_CONVERSATION_ID and for advisor construction — and re-read the callout above before you reach for CONVERSATION_ID.
Re-test structured output against the real model. Schema changes are silent.
Integration-test anything that serializes. Jackson 3 changes date format and property order.
Steps 1 to 3 are compiler-driven and finite. Steps 4 and 5 are where the actual risk is, because nothing fails until a user notices.
Reproducing this
git clone https://ankurm.com/git.app/asmhatre/spring-ai-2-migration.git
cd spring-ai-2-migration
mvn test
Five tests, under a second, and no API key. A canned ChatModel returns fixed responses; because ChatClient, advisors and chat memory all sit above the model, everything that changed is still exercised faithfully. The fake model records every Prompt it receives, so the tests assert what Spring AI sent rather than what a model happened to reply — which is the only assertion that is stable enough to be worth writing.
The complete breaking-change reference in the repository covers everything above plus the smaller items that did not fit here.
Further reading
spring-ai-2-migration — the companion repository: full breaking-change reference and the offline test suite
No Comments yet!