-
Notifications
You must be signed in to change notification settings - Fork 362
Spring Boot 4 Spring AI 2.0 Migration
Comprehensive write-up of the platform upgrade that brought the agent stack from Spring Boot 3.x onto the Boot 4 line.
Branch:
2.0.0| Scope:embabel-agentreactor +embabel-buildBOM | Status: build green; one runtime regression under investigation (see Known issues)
| Dependency | Before | After | Notes |
|---|---|---|---|
| Spring Boot | 3.x | 4.0.6 | Many starters split into smaller modules (jackson, security, observation, webmvc-test) |
| Spring Framework | 6.x | 7.x (via Boot) |
JSpecify @NullMarked tightens generics; core.retry.RetryTemplate introduced |
| Spring AI | 1.x | 2.0.0-M8 | Hand-rolled OpenAiApi / AnthropicApi removed in favour of vendor SDKs |
| Jackson | 2.x | 3.x | Package rebrand com.fasterxml.jackson.* → tools.jackson.*; ObjectMapper is now immutable
|
| Kotlin | 2.1.10 | 2.2.21 (K2) | Stricter K2 type-inference around JSpecify |
| JUnit | 5.x | 6.x |
assertThrows<X?> no longer accepts nullable type args |
| spring-retry | from Boot BOM | 2.0.12 (direct dep) | Boot 4 dropped it from the BOM; kept the library to avoid migrating to core.retry
|
| openai-java | 4.16.1 (test) | 4.36.0 (compile) | Now used on the main classpath since Spring AI exposes OpenAIClient directly |
| anthropic-java | — | 2.34.0 (via Spring AI) | First-class dep introduced by Spring AI 2.0 |
| MCP SDK | 1.x | 2.x |
McpSyncClientCustomizer + McpAsyncClientCustomizer unified into McpClientCustomizer
|
The migration landed across the embabel-agent 2.0.0 branch in five
commits (plus one prep commit in embabel-build):
embabel-agent (newest → oldest):
f681b4c Spring AI 2.0 — rewrite OpenAI + Anthropic factories on native SDKs
dd432fa Spring Boot 4 + AI 2.0 follow-ups across autoconfigure modules
3db2a60 Spring Boot 4 + AI 2.0 follow-ups: webmvc-test dep, MCP customizer
a176c1c upgrade to Spring Boot 4.0.6 + Spring AI 2.0.0-M8 + Jackson 3 ← foundational
embabel-build (BOM):
dca045e Spring Boot 4 / AI 2.0 build deps: openai-java 4.36 + JSpecify ignore flag
abe1034 Update Kotlin to version 2.2.21
7d45753 Upgrade to Spring AI 2.0.0-M8
68f650d Update Spring Boot Version to 4.0.6
Total scope across the four embabel-agent commits: ~35 files, +376 / −220 lines (excluding the foundational commit, which is larger).
Spring AI 2.0 deleted its hand-rolled provider facades (OpenAiApi,
AnthropicApi, OpenAiModerationApi) and now delegates to the vendor SDKs
(openai-java, anthropic-java) directly. This is the single most invasive
change: every chat-model factory in the platform was rewritten.
Builder API renames (apply to all *ChatModel.Builder types in Spring AI 2.0):
| Spring AI 1.x | Spring AI 2.0 |
|---|---|
.openAiApi(OpenAiApi) |
.openAiClient(OpenAIClient) |
.anthropicApi(AnthropicApi) |
.anthropicClient(AnthropicClient) |
.defaultOptions(ChatOptions) |
.options(ChatOptions) |
.retryTemplate(spring-retry RetryTemplate) |
removed (core.retry.RetryTemplate accepted on some, but we drop the call) |
Retry policy note. We no longer pass a retry template to the model builder — retries are wrapped one layer up at
ChatClientLlmOperationsviaspring-retry, which is the same place async streaming retries are already handled.
| Module | What changed |
|---|---|
embabel-agent-openai |
OpenAiCompatibleModelFactory rewritten; builds OpenAIClient via OpenAIOkHttpClient.builder(). Ctor parameters restClientBuilder / webClientBuilder / completionsPath / embeddingsPath / retryTemplate kept as @Suppress("UNUSED_PARAMETER") no-ops for source compatibility with the 5 downstream subclasses. OpenAiEmbeddingModel switched to the new 4-arg ctor (OpenAIClient, MetadataMode, Options, ObservationRegistry). |
embabel-agent-anthropic-autoconfigure |
AnthropicModelFactory rewritten; builds AnthropicClient via AnthropicOkHttpClient.builder() (chosen over AnthropicSetup.setupSyncClient to avoid the SDK's implicit ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY env-var precedence — our factory's caller has already resolved env vs. property). |
embabel-agent-dockermodels-autoconfigure |
DockerLocalModelsConfig rewritten; same migration off OpenAiApi.Builder() to OpenAIOkHttpClient.builder(). Uses apiKey("no-auth") placeholder since the openai-java SDK rejects null/blank keys but the local Docker server doesn't require auth. |
embabel-agent-bedrock-autoconfigure |
BedrockModelsConfig switched from Jackson 2 ObjectMapper to Jackson 3 tools.jackson.databind.json.JsonMapper.builder().build(). |
embabel-agent-deepseek-autoconfigure |
Dropped redundant .retryTemplate(RetryTemplate) builder call (Spring AI 2.0 expects org.springframework.core.retry.RetryTemplate now; we wrap with spring-retry elsewhere). |
embabel-agent-google-genai-autoconfigure |
Pass-through RetryTemplate arg now an empty org.springframework.core.retry.RetryTemplate() (positional, can't drop). |
embabel-agent-mistral-ai-autoconfigure |
Dropped redundant .retryTemplate(...) (same as deepseek). |
Spring AI 2.0 promoted several types out of their old .api subpackage:
| Old | New |
|---|---|
org.springframework.ai.anthropic.api.AnthropicCacheTtl |
org.springframework.ai.anthropic.AnthropicCacheTtl |
org.springframework.ai.anthropic.api.AnthropicCacheOptions |
org.springframework.ai.anthropic.AnthropicCacheOptions |
org.springframework.ai.anthropic.api.AnthropicCacheStrategy |
org.springframework.ai.anthropic.AnthropicCacheStrategy |
-
Anthropic thinking config.
AnthropicApi.ChatCompletionRequest.ThinkingConfig(ThinkingType.ENABLED, budget)is gone. Use the first-class builder methods:AnthropicChatOptions.builder().thinkingEnabled(tokenBudget.toLong())or.thinkingDisabled(). -
Anthropic native usage.
AnthropicApi.Usage(withintaccessors) →com.anthropic.models.messages.Usage(withOptional<Long>accessors).UsageExtensionspreserves theInt?return shape via.orElse(null)?.toInt()so call sites are unchanged. -
OpenAI moderation.
OpenAiModerationApiis gone — moderation goes throughOpenAiModerationModel.builder().openAiClient(client).build(). -
OpenAI HTTP overrides.
completionsPath/embeddingsPathare no longer honoured by the SDK — bake the path intobaseUrlinstead. The factory logs a warning if these are set. -
OpenAI custom headers.
OpenAiChatOptions.Builder.httpHeaders(Map)renamed to.customHeaders(Map).
Boot 4 split several umbrella starters into smaller modules. Each split required an explicit dep declaration where we used the symbol:
| Symbol | Previously | Now in module | Where we added the dep |
|---|---|---|---|
JacksonAutoConfiguration |
spring-boot-autoconfigure | spring-boot-jackson |
embabel-agent-bedrock-autoconfigure + embabel-agent-openai-autoconfigure test scope |
ObservationRegistryCustomizer |
spring-boot-actuator-autoconfigure | spring-boot-micrometer-observation |
embabel-agent-observability-autoconfigure |
SecurityAutoConfiguration |
spring-boot-autoconfigure | spring-boot-security |
embabel-agent-mcpserver-security-autoconfigure (test) — note no .servlet segment in the new FQN |
OAuth2ResourceServerAutoConfiguration |
spring-boot-autoconfigure | spring-boot-security-oauth2-resource-server |
embabel-agent-mcpserver-security-autoconfigure (test) |
@AutoConfigureMockMvc |
spring-boot-test-autoconfigure | spring-boot-webmvc-test |
embabel-agent-webmvc + embabel-agent-a2a (test) |
Jackson2ObjectMapperBuilder |
available by default | removed | Build mapper via jacksonObjectMapper() directly |
aspectjweaver |
pulled by spring-boot-starter-aop
|
direct dep | Boot 4 renamed the starter |
Jackson 3 is a hard fork with two breaking themes:
Package rebrand. com.fasterxml.jackson.databind/module/dataformat/... →
tools.jackson.databind/module/dataformat/.... Annotations stayed at
com.fasterxml.jackson.annotation.* (per Jackson team's note: "Annotations
remain at Jackson 2.x group id").
Immutability. ObjectMapper is no longer mutable — you can't call
.registerModule(...) on a constructed instance to chain modules. Use
JsonMapper.builder().addModule(...).build() (or call helpers like
jacksonObjectMapper() that return a pre-configured mapper).
| Concern | Migration |
|---|---|
| Kotlin module |
jacksonObjectMapper() from tools.jackson.module.kotlin already wires up KotlinModule — drop .registerKotlinModule()
|
| Java Time (jsr310) |
Built into databind 3 — drop the jackson-datatype-jsr310 dep and the .registerModule(JavaTimeModule()) call |
| Custom deserializers |
JsonDeserializer → ValueDeserializer; ContextualDeserializer merged in |
| Field/property names |
fieldName() / fieldNames() → propertyName() / propertyNames()
|
| Feature relocations |
WRITE_DATES_AS_TIMESTAMPS moved to DateTimeFeature
|
| Lenient mapper | Build via JsonMapper.builder().lenientMode(true).build() pattern (no in-place .rebuild()) |
| Exception types | Unserializable graphs now throw IllegalArgumentException / JacksonException instead of IOException — loosened test assertions |
| ArrayNode iteration |
ArrayNode.asString() semantics changed; flattened in two test sites |
| Schema generation |
victools/jsonschema-generator bumped to 5.0.0 (Jackson 3 native, pulled transitively by Spring AI 2.0); ObjectNode.deepCopy() returns ObjectNode directly (no type arg); schema required field may now nest arrays — flattened in two test sites |
Spring 7, Spring AI 2.0, Jackson 3, and JUnit 6 all moved to @NullMarked
JSpecify packages. With Kotlin 2.2 K2's strict type-inference, this
cascades non-null T : Any upper bounds into every override of those
APIs — breaking the agent platform's many <T> / <O> signatures.
Decision: suppress JSpecify enforcement rather than do an Any-bound sweep.
<!-- embabel-build/pom.xml, kotlin-maven-plugin args -->
<arg>-Xjspecify-annotations=ignore</arg>This keeps the existing <T> / <O> signatures everywhere. Where the
boundary couldn't be avoided (Spring AI's StructuredOutputConverter etc.),
we kept <T : Any> bounds in 7 converter classes and added explicit
Class<Any> casts at the 4 converter construction sites that build them
from unbounded <O> enclosing methods.
Other small Spring 7 fallouts: AutoRegistration.ApplicationListener<ContextRefreshedEvent>
dropped its redundant ? per Spring 7's stricter signatures.
-
ChatClient.options()now takesChatOptions.Builder<*>— we bakechatOptionsinto thePromptitself (preserves theToolCallingChatOptionssubtype through the advisor chain). - Tool callbacks are embedded via
ToolCallingChatOptions.Builderso they survive the chat-model-defaults merge in addition to the spec-level.toolCallbacks()call (two paths to be safe). -
Generation.getResult()/Message.getText()are now nullable — added the required!!/?:dereferences at call sites. -
Usageis non-null in Spring AI 2.0 — updated test mocks. -
StructuredOutputConverteraddedgetJsonSchema()—JacksonOutputConverterexposes it via a private val. -
ModelOptionsUtils.jsonToMapremoved — replaced withobjectMapper.readValue. - Tool-callback assertions: 3 tests had to be relaxed because Spring AI 2.0
routes spec
toolCallbacksthroughToolCallAdvisorinternally rather than mirroring them onto the finalprompt.optionsseen byChatModel.call().
-
McpSyncClientCustomizer+McpAsyncClientCustomizerunified intoMcpClientCustomizer(single interface, sync/async via separate methods). -
McpSchema.Resourcector gainedtitle/size/metaparams —SyncResourceSpecificationFactorypassesnameas title,0Lsize,nullmeta. -
clientCapabilitiesis non-null in test mocks now.
-
openai-javaversion bumped4.16.1→ 4.36.0 (matches whatspring-ai-openai 2.0.0-M8is compiled against) and moved fromtestscope to default (compile) scope soOpenAIClientis available on the main classpath. -
openai-java-client-okhttpadded todependencyManagement(same version). - Fixed a
${openai-java.version>}HTML-escape typo inembabel-build-dependencies/pom.xmlthat broke variable resolution.
The table below summarizes everything touched. Modules not listed compiled without changes.
| Module | Changes |
|---|---|
embabel-agent-api |
ChatClientLlmOperations: spring-retry pattern preserved (.execute<X, DatabindException>, RetrySynchronizationManager.getContext()?.retryCount + 1); chatOptions baked into Prompt + chain .toolCallbacks(springAiToolCallbacks); Class<Any> casts on converter chains. SpringAiLlmMessageStreamer, StreamingChatClientOperations: same dual-path tool callback pattern. 7 converter files keep <T : Any> bounds. spring-retry 2.0.12 added as direct dep. |
embabel-agent-openai |
OpenAiCompatibleModelFactory fully rewritten — see §3.1. POM pins openai-java + openai-java-client-okhttp 4.36.0 compile-scope and adds spring-webflux back (Spring AI 2.0 dropped its webflux dep). |
embabel-agent-mcp/embabel-agent-mcpserver |
SyncResourceSpecificationFactory: McpSchema.Resource ctor — added name as title, 0L size, null meta. |
| Module | Changes |
|---|---|
embabel-agent-platform-autoconfigure |
AgentPlatformConfiguration.embabelJacksonObjectMapper() returns jacksonObjectMapper() directly (Jackson2ObjectMapperBuilder removed in Boot 4); dropped defaultCandidate = false for autowire. |
embabel-agent-shell-autoconfigure |
AgentShellProperties: javax.validation → jakarta.validation (3 imports). |
embabel-agent-observability-autoconfigure |
MicrometerTracingAutoConfiguration: ObservationRegistryCustomizer import → o.s.b.micrometer.observation.autoconfigure.*; added spring-boot-micrometer-observation dep; gated defaultTracingObservationHandler with @ConditionalOnBean(Tracer.class). HttpBodyCachingFilter: ContentCachingRequestWrapper(request, Integer.MAX_VALUE) 2-arg ctor (prod + 3 tests). |
embabel-agent-mcpserver-security-autoconfigure |
Added spring-boot-security + spring-boot-security-oauth2-resource-server test deps. AgentMcpServerSecurityAutoConfigurationTest imports: o.s.b.security.autoconfigure.SecurityAutoConfiguration (no .servlet) + o.s.b.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration. |
embabel-agent-a2a |
Added spring-boot-webmvc-test test dep. A2AWebIntegrationTest: @AutoConfigureMockMvc import relocated to o.s.b.webmvc.test.autoconfigure. |
embabel-agent-webmvc |
Added spring-boot-webmvc-test test dep. 5 test files: same MockMvc import relocation. |
| Module | Changes |
|---|---|
embabel-agent-openai-autoconfigure |
Calls go through rewritten OpenAiCompatibleModelFactory. Added spring-boot-jackson test dep for JacksonAutoConfiguration relocation. LLMOpenAiGuardRailsIntegrationIT: OpenAiModerationApi removed → OpenAiModerationModel.builder().openAiClient(...). Two ITs: JacksonAutoConfiguration import relocated. |
embabel-agent-anthropic-autoconfigure |
AnthropicModelFactory + AnthropicModelsConfig + AnthropicCachingConfig + UsageExtensions rewritten (see §3.1). Test fixtures: AnthropicOptionsConverterTest updated for ThinkingConfigParam.isEnabled() / isDisabled() as functions; UsageExtensionsTest gets an anthropicUsage(...) helper that wraps Usage.builder() to keep the prior positional ergonomics. |
embabel-agent-bedrock-autoconfigure |
BedrockModelsConfig: ObjectMapper → JsonMapper. Added spring-boot-jackson test dep. AgentBedrockAutoConfigurationIT: JacksonAutoConfiguration import relocated. |
embabel-agent-dockermodels-autoconfigure |
DockerLocalModelsConfig rewritten off OpenAiApi.Builder(). |
embabel-agent-deepseek-autoconfigure |
Dropped redundant .retryTemplate(properties.retryTemplate("deepseek-${modelDef.modelId}")) from DeepSeekChatModel.builder() chain. |
embabel-agent-google-genai-autoconfigure |
Pass-through RetryTemplate arg now empty o.s.core.retry.RetryTemplate() (positional). |
embabel-agent-mistral-ai-autoconfigure |
Dropped redundant .retryTemplate(...) (same as deepseek). |
| File | Change |
|---|---|
Root pom.xml
|
Re-enabled embabel-agent-openai (briefly disabled mid-migration while the OpenAI rewrite was outstanding). |
embabel-agent-autoconfigure/pom.xml |
Re-enabled 7 model autoconfigure modules disabled mid-migration: openai*, openai-custom*, anthropic*, lmstudio*, gemini*, dockermodels*, minimax*. |
embabel-agent-starters/pom.xml |
Re-enabled the 7 matching starter modules. |
| File | Change |
|---|---|
embabel-build-dependencies/pom.xml |
openai-java.version 4.16.1 → 4.36.0; dropped test scope; added openai-java-client-okhttp managed entry; fixed > HTML-escape typo. |
embabel-build/pom.xml (parent) |
Added -Xjspecify-annotations=ignore to kotlin-maven-plugin args. |
Before Spring AI 2.0 we had two retry layers:
- The Spring AI
*ChatModel.Builder.retryTemplate(...)— pinned viaRetryProperties.retryTemplate("<provider>-<model>"). - The
ChatClientLlmOperationswrapper —RetryTemplate.execute<X, DatabindException>.
Spring AI 2.0 dropped layer 1 (or changed its type to core.retry.RetryTemplate,
which we don't want to migrate to). The RetryProperties config keys are
still honoured — they just now configure layer 2 only. Effective retry
behaviour is unchanged.
The openai-java and anthropic-java SDKs use OkHttp internally. Spring's
RestClient.Builder / WebClient.Builder are no longer wired into the
HTTP path — they're accepted as ctor params on OpenAiCompatibleModelFactory
and AnthropicModelFactory purely for source compatibility with the
5 + 1 downstream subclasses that pass them positionally. The params are
@Suppress("UNUSED_PARAMETER") no-ops.
If you were using these to install custom interceptors, request factories, or observation registries on the LLM HTTP path, that integration is gone for these two providers (other providers — Mistral, DeepSeek, Bedrock — still use Spring HTTP clients).
The openai-java SDK uses fixed endpoint paths (/v1/chat/completions,
/v1/embeddings). The completionsPath / embeddingsPath ctor params
are still accepted but log a warning at startup and are ignored. If you
were targeting an OpenAI-compatible provider with a non-standard endpoint
path (e.g. Groq's /openai/v1/chat/completions), bake the full path into
baseUrl instead.
The openai-java SDK rejects null/blank API keys at build time even if the
backing server doesn't require auth. OpenAiCompatibleModelFactory passes
the placeholder string "no-auth" when apiKey is null, so local
OpenAI-compatible servers (LM Studio, Docker Models) still work.
Any code that does ObjectMapper().registerModule(...) or
mapper.registerKotlinModule() post-construction will not compile in
Jackson 3. Use one of:
// Pre-configured with Kotlin module
import tools.jackson.module.kotlin.jacksonObjectMapper
val om = jacksonObjectMapper()
// Or build explicitly
import tools.jackson.databind.json.JsonMapper
val om = JsonMapper.builder()
.addModule(myCustomModule)
.build()JSR-310 Java Time support is built into Jackson 3 databind — no
separate jackson-datatype-jsr310 dep, no .registerModule(JavaTimeModule()).
Spring AI 2.0 routes spec-level .toolCallbacks(...) through
ToolCallAdvisor internally rather than mirroring them onto the final
prompt.options.toolCallbacks visible at ChatModel.call(). Three test
assertions had to be relaxed with comments. If you have similar tests,
verify intent and either:
- check callbacks via the spec recorder, or
- assert on the call's effect rather than the prompt's shape.
JUnit 6 with @NullMarked rejects nullable type arguments on assertThrows.
Drop the ?:
// Before
assertThrows<IllegalStateException?> { ... }
// After
assertThrows<IllegalStateException> { ... }BedrockModelsConfig now uses tools.jackson.databind.json.JsonMapper.builder().build()
explicitly. Spring AI 2.0's Bedrock client expects a Jackson 3 JsonMapper,
not a generic Jackson 2 ObjectMapper.
If you're an app on top of embabel-agent that just consumes the platform,
you'll typically need:
| Symptom | Fix |
|---|---|
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration missing |
Add spring-boot-jackson dep; import becomes o.s.b.jackson.autoconfigure.JacksonAutoConfiguration
|
javax.validation.* not found |
Switch to jakarta.validation.* (Boot 3 → 4 cleanup completion) |
com.fasterxml.jackson.databind.ObjectMapper not found |
Switch to tools.jackson.databind.ObjectMapper (or just call jacksonObjectMapper() from tools.jackson.module.kotlin) |
ObjectMapper.registerKotlinModule() returns void, can't chain |
Replace with jacksonObjectMapper() — the helper returns a pre-configured mapper |
JavaTimeModule import unresolved |
Drop it — Jackson 3 builds JSR-310 support in |
OpenAiApi / AnthropicApi not found |
Use the vendor SDK directly: OpenAIOkHttpClient.builder() / AnthropicOkHttpClient.builder()
|
OpenAiChatModel.Builder.defaultOptions(...) not found |
Rename to .options(...)
|
AnthropicApi.ChatCompletionRequest.ThinkingConfig not found |
Use AnthropicChatOptions.Builder.thinkingEnabled(tokens.toLong()) / thinkingDisabled()
|
org.springframework.ai.anthropic.api.AnthropicCache* not found |
Drop the .api segment from the import |
assertThrows<X?> won't compile |
Drop the ?
|
Tracer bean not found at startup |
Add a tracing bridge (micrometer-tracing-bridge-brave or micrometer-tracing-bridge-otel) OR rely on the new @ConditionalOnBean(Tracer.class) on defaultTracingObservationHandler
|
@AutoConfigureMockMvc import unresolved in tests |
Add spring-boot-webmvc-test dep; import moves to o.s.b.webmvc.test.autoconfigure
|
McpSyncClientCustomizer / McpAsyncClientCustomizer unresolved |
Replace with unified McpClientCustomizer
|
When examples-java is launched with the anthropic-models Maven profile
active (ANTHROPIC_API_KEY set), startup fails:
Error creating bean with name 'com.embabel.agent.config.models.anthropic.AnthropicModelsConfig':
Failed to instantiate: Constructor threw exception
The truncated log doesn't include the Caused by chain. Diagnosis is open
(task #37). Run with --debug and capture the full stack trace; the most
likely candidates are an SDK-level validation in AnthropicOkHttpClient.build()
or an unintended eager init path on the autoconfigure class.
The middle commit (dd432fa) temporarily disabled the following modules
from the reactor while the OpenAI / Anthropic rewrites were outstanding:
embabel-agent-openaiembabel-agent-{openai,openai-custom,anthropic,lmstudio,gemini,dockermodels,minimax}-autoconfigure- The matching
embabel-agent-starter-*modules
All are re-enabled in the final commit (f681b4c). If you cherry-pick
commits out of order, you may need to re-enable manually — search the
parent POMs for TEMPORARILY DISABLED markers (none should remain on
2.0.0 HEAD).
Boot 4 / Spring 7 surfaced several pre-existing post-processor-ordering warnings that were silent before. Examples:
Bean 'embeddingTrackingConfiguration' is not eligible for getting processed by all BeanPostProcessors
Bean 'org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration' is not eligible ...
These are noisy but not fatal. Fix by declaring the offending
BeanPostProcessor factory methods as static (per the warning's hint)
or marking the bean with BeanDefinition.ROLE_INFRASTRUCTURE.
- Spring Boot 4.0 release notes: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes
- Spring AI 2.0 migration guide: https://docs.spring.io/spring-ai/reference/2.0/upgrade-notes.html
- Jackson 3 release notes: https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.x
- Kotlin 2.2 K2 release: https://kotlinlang.org/docs/whatsnew22.html
- JSpecify on Spring: https://docs.spring.io/spring-framework/reference/core/null-safety.html
- openai-java SDK: https://github.com/openai/openai-java
- anthropic-java SDK: https://github.com/anthropics/anthropic-sdk-java
(c) Embabel Software Inc 2024-2025.