diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 8d356fe..ebd2e7b 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -2,6 +2,6 @@
-
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 586ac49..26c1610 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,9 @@ Guidance for AI coding assistants working in this repository.
`DataSource-Extensions` is a multi-module Gradle (Kotlin DSL) library that wraps Caplin's `com.caplin.platform.integration.java:datasource` SDK with modern reactive APIs and a Spring Boot starter. Kotlin Coroutines `Flow` is the canonical internal representation; Java `Flow.Publisher` and Reactive Streams `Publisher` variants are thin adapters over it.
-JDK 17. Spring Boot pinned to 3.5.x and Kotlin to 2.2.x (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.
+JDK 17. Two parallel release lines (see the compatibility table in `README.md`): **`main` targets Spring Boot 4.0.x** (Jackson 3 is the default JSON binding; Jackson 2 is a `compileOnly` opt-in), and **`springboot-3.5.x` is the Spring Boot 3.5.x maintenance branch** (Jackson 2 default). Kotlin pinned to 2.2.21 (see `gradle/libs.versions.toml`). The `common-library` convention plugin applies `io.spring.dependency-management` and overrides Spring's BOM `kotlin.version` to our catalog value — without this, transitive Jackson updates raise `kotlin-stdlib` past what the compiler can read.
+
+`datasourcex-util` ships both Jackson serialization layers under `serialization/{jackson2,jackson3}` (mirrored serializers + a `JsonHandler` each, sharing zjsonpatch for RFC 6902 diff/patch). On `main`, Jackson 3 (`tools.jackson.*`) is the runtime default and Jackson 2 (`com.fasterxml.jackson.*`) is `compileOnly`; on `springboot-3.5.x` it's the reverse. The Spring starter's `JsonHandler` bean auto-selects Jackson 3 when present and falls back to Jackson 2 (`DataSourceAutoConfiguration`).
## Common commands
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..03ccb7d
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,60 @@
+# Migration guide
+
+## 2.x (Spring Boot 3.5) → 3.x (Spring Boot 4.0)
+
+Version `3.x` targets **Spring Boot 4.0** and makes **Jackson 3** (`tools.jackson.*`) the default
+JSON binding, matching Spring Boot 4's own default. To stay on Spring Boot 3.5, remain on the
+[`2.x` line](./README.md#compatibility) (branch `springboot-3.5.x`).
+
+### Prerequisites
+
+- Spring Boot **4.0.x**
+- Kotlin **2.2.21+**
+- JDK **17+** (unchanged)
+
+### 1. Bump the dependency
+
+```kotlin
+dependencies {
+ implementation("com.caplin.integration.datasourcex:spring-boot-starter-datasource:3.+")
+ // or, for the reactive / util modules:
+ implementation("com.caplin.integration.datasourcex:datasourcex-kotlin:3.+")
+}
+```
+
+### 2. Jackson 3 is now the default
+
+Spring Boot 4 auto-configures a Jackson 3 `tools.jackson.databind.ObjectMapper`, and the starter
+wires a Jackson-3-backed `JsonHandler` onto the DataSource by default. For most applications no
+change is required — plain POJOs serialize as before.
+
+If you registered **custom Jackson 2 modules, serializers, or `ObjectMapper` customizers**, port them
+to Jackson 3. See the
+[Jackson 3 release notes](https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.0).
+
+### 3. Keeping Jackson 2 (optional)
+
+To keep using Jackson 2 for DataSource JSON, add Spring Boot's (deprecated) Jackson 2 module and
+define your own `JsonHandler` bean. The starter's handler is `@ConditionalOnMissingBean`, so yours
+takes precedence:
+
+```kotlin
+// build.gradle.kts
+implementation("org.springframework.boot:spring-boot-jackson2")
+```
+
+```kotlin
+import com.caplin.datasource.messaging.json.JsonHandler
+import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.springframework.context.annotation.Bean
+
+@Bean
+fun dataSourceJsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
+ SimpleDataSourceFactory.createJackson2JsonHandler(objectMapper)
+```
+
+Outside Spring, `SimpleDataSourceFactory.createDataSource(...)` now defaults to the Jackson 3
+handler; pass `SimpleDataSourceFactory.defaultJackson2JsonHandler` (or your own) explicitly to keep
+Jackson 2. On the `3.x` line the Jackson 2 artifacts are `compileOnly` in `datasourcex-util`, so add
+them to your own classpath if you use the Jackson 2 helpers directly.
diff --git a/README.md b/README.md
index 8fb1c1a..c04dacb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,19 @@
# Extensions for DataSource
-Requires Kotlin 2.2 or later.
+Requires Kotlin 2.2 or later and JDK 17 or later.
+
+## Compatibility
+
+This library is maintained in two parallel lines — choose the version that matches your Spring Boot
+major version:
+
+| Library version | Branch | Spring Boot | Default JSON binding | Kotlin |
+|------------------|---------------------|-------------|---------------------------------------------------------------------------------------------------------------|---------|
+| `3.x` | `main` | 4.0.x | Jackson 3 (Jackson 2 available via [`spring-boot-jackson2`](https://docs.spring.io/spring-boot/reference/features/json.html)) | 2.2.21+ |
+| `2.x` | `springboot-3.5.x` | 3.5.x | Jackson 2 (Jackson 3 available by adding the `tools.jackson` dependencies) | 2.2+ |
+
+Upgrading from `2.x` (Spring Boot 3.5) to `3.x` (Spring Boot 4.0)? See the
+[migration guide](./MIGRATION.md).
## Reactive
@@ -32,7 +45,7 @@ Then refer to the documentation:
## Spring
This module provides a starter for integrating Caplin DataSource with your
-[Spring Boot 3.5](https://spring.io/projects/spring-boot) application, and integration with
+[Spring Boot 4.0](https://spring.io/projects/spring-boot) application, and integration with
[Spring Messaging](https://docs.spring.io/spring-boot/docs/current/reference/html/messaging.html)
for publishing data from annotated functions.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 70bdfb3..c2d886e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,8 +1,7 @@
[versions]
-springBoot = "3.5.14"
-kotlin = "2.2.0"
+springBoot = "4.0.6"
+kotlin = "2.2.21"
kotlinCollectionsImmutable = "0.4.0"
-jackson3 = "3.1.3"
zjsonpatch = "0.6.2"
jmh = "1.37"
jmh-plugin = "0.7.3"
@@ -31,8 +30,6 @@ spring-dependency-management-plugin = "1.1.7"
[libraries]
kotlin-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinCollectionsImmutable" }
-jackson3-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson3" }
-jackson3-module-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson3" }
zjsonpatch = { module = "io.github.vishwakarma:zjsonpatch", version.ref = "zjsonpatch" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
fory-core = { module = "org.apache.fory:fory-core", version.ref = "fory" }
diff --git a/spring/build.gradle.kts b/spring/build.gradle.kts
index b19291f..1608b4e 100644
--- a/spring/build.gradle.kts
+++ b/spring/build.gradle.kts
@@ -14,7 +14,10 @@ dependencies {
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-json")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ implementation("tools.jackson.module:jackson-module-kotlin")
+ // Jackson 2 is only needed to compile the jackson2 fallback in DataSourceAutoConfiguration; it is
+ // present at runtime only if the consumer adds spring-boot-jackson2.
+ compileOnly("com.fasterxml.jackson.core:jackson-databind")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.slf4j:slf4j-api")
diff --git a/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/annotations/DataMessageMapping.kt b/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/annotations/DataMessageMapping.kt
index 05532d1..34f0b0d 100644
--- a/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/annotations/DataMessageMapping.kt
+++ b/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/annotations/DataMessageMapping.kt
@@ -20,8 +20,8 @@ annotation class DataMessageMapping(
* Methods annotated with this message should return one or more objects to be serialized to
* JSON.
*
- * The JSON handler installed in [DataSource] will be used. By default, this is the
- * [com.fasterxml.jackson.databind.ObjectMapper] provided by Spring Boot.
+ * The JSON handler installed in [DataSource] will be used. By default, this is backed by the
+ * [tools.jackson.databind.ObjectMapper] provided by Spring Boot.
*/
JSON,
diff --git a/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfiguration.kt b/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfiguration.kt
index 89b4e4e..d6f0b65 100644
--- a/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfiguration.kt
+++ b/spring/src/main/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfiguration.kt
@@ -9,16 +9,19 @@ import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Discovery
import com.caplin.integration.datasourcex.util.SimpleDataSourceConfig.Peer
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createDataSource
import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson2JsonHandler
+import com.caplin.integration.datasourcex.util.SimpleDataSourceFactory.createJackson3JsonHandler
import com.caplin.integration.datasourcex.util.getLogger
-import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.ObjectMapper as Jackson2ObjectMapper
import java.nio.file.Paths
import java.util.UUID
import java.util.logging.Logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.AutoConfiguration
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
+import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper
@AutoConfiguration
@EnableConfigurationProperties(DataSourceConfigurationProperties::class)
@@ -31,9 +34,20 @@ internal class DataSourceAutoConfiguration {
internal const val DEFAULT_DATASOURCE_NAME = "caplin-adapter"
}
+ // Prefer Jackson 3 (the Spring Boot 4 default). Falls back to Jackson 2 only when Jackson 3 is
+ // absent and a Jackson 2 ObjectMapper is present (e.g. the consumer added spring-boot-jackson2).
+ // Both are @ConditionalOnMissingBean(JsonHandler) so a consumer-defined JsonHandler wins
+ // outright.
@Bean
- @ConditionalOnMissingBean
- fun jsonHandler(objectMapper: ObjectMapper): JsonHandler<*> =
+ @ConditionalOnClass(Jackson3ObjectMapper::class)
+ @ConditionalOnMissingBean(JsonHandler::class)
+ fun jackson3JsonHandler(objectMapper: Jackson3ObjectMapper): JsonHandler<*> =
+ createJackson3JsonHandler(objectMapper)
+
+ @Bean
+ @ConditionalOnClass(Jackson2ObjectMapper::class)
+ @ConditionalOnMissingBean(JsonHandler::class)
+ fun jackson2JsonHandler(objectMapper: Jackson2ObjectMapper): JsonHandler<*> =
createJackson2JsonHandler(objectMapper)
@Bean
diff --git a/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfigurationTest.kt b/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfigurationTest.kt
new file mode 100644
index 0000000..87256f5
--- /dev/null
+++ b/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceAutoConfigurationTest.kt
@@ -0,0 +1,46 @@
+package com.caplin.integration.datasourcex.spring.internal
+
+import com.caplin.datasource.DataSource
+import com.caplin.datasource.messaging.json.JsonHandler
+import com.caplin.integration.datasourcex.util.serialization.jackson3.Jackson3JsonHandler
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.extensions.spring.SpringExtension
+import io.kotest.matchers.types.shouldBeInstanceOf
+import io.mockk.mockk
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.test.context.TestPropertySource
+import tools.jackson.databind.ObjectMapper as Jackson3ObjectMapper
+import tools.jackson.databind.json.JsonMapper
+
+/**
+ * Verifies the autoconfiguration selects the Jackson 3 [JsonHandler] when a Jackson 3
+ * [ObjectMapper] is present — the Spring Boot 4 default. [ImportAutoConfiguration] loads the
+ * autoconfiguration with proper ordering so its `@ConditionalOnMissingBean` conditions see the
+ * test's beans first, letting us mock the DataSource rather than create a real one.
+ */
+@SpringBootTest(classes = [DataSourceAutoConfigurationTest.TestConfig::class])
+@ImportAutoConfiguration(DataSourceAutoConfiguration::class)
+@TestPropertySource(properties = ["caplin.datasource.managed.discovery.hostname=localhost"])
+class DataSourceAutoConfigurationTest : FunSpec() {
+
+ @Autowired private lateinit var jsonHandler: JsonHandler<*>
+
+ init {
+ extension(SpringExtension())
+
+ test("wires the Jackson 3 JsonHandler by default") {
+ jsonHandler.shouldBeInstanceOf()
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ class TestConfig {
+ @Bean fun jackson3ObjectMapper(): Jackson3ObjectMapper = JsonMapper.builder().build()
+
+ @Bean fun dataSource(): DataSource = mockk(relaxed = true)
+ }
+}
diff --git a/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceEndToEndTest.kt b/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceEndToEndTest.kt
index 3d52e67..63105dc 100644
--- a/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceEndToEndTest.kt
+++ b/spring/src/test/kotlin/com/caplin/integration/datasourcex/spring/internal/DataSourceEndToEndTest.kt
@@ -9,8 +9,6 @@ import com.caplin.integration.datasourcex.spring.annotations.DataMessageMapping.
import com.caplin.integration.datasourcex.spring.annotations.DataService
import com.caplin.integration.datasourcex.spring.annotations.IngressDestinationVariable
import com.caplin.integration.datasourcex.spring.annotations.IngressToken.USER_ID
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.extensions.spring.SpringExtension
@@ -29,6 +27,8 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.handler.annotation.Payload
import org.springframework.test.context.TestPropertySource
+import tools.jackson.databind.ObjectMapper
+import tools.jackson.module.kotlin.jacksonMapperBuilder
/**
* End-to-end test of the Spring Boot starter: a real application context wires the
@@ -176,7 +176,7 @@ class DataSourceEndToEndTest : FunSpec() {
@Bean fun dataSource(): DataSource = fake.dataSource
- @Bean fun objectMapper(): ObjectMapper = jacksonObjectMapper()
+ @Bean fun objectMapper(): ObjectMapper = jacksonMapperBuilder().build()
}
private companion object {
diff --git a/util/build.gradle.kts b/util/build.gradle.kts
index 0eaf024..073cb35 100644
--- a/util/build.gradle.kts
+++ b/util/build.gradle.kts
@@ -14,16 +14,19 @@ dependencies {
api("org.slf4j:slf4j-api")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm")
- api("com.fasterxml.jackson.core:jackson-core")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ // Jackson 3 is the default JSON binding on this (Spring Boot 4) line; versions come from the
+ // Spring Boot BOM.
+ api("tools.jackson.core:jackson-databind")
+ implementation("tools.jackson.module:jackson-module-kotlin")
implementation(libs.zjsonpatch)
- // Jackson 3 support is opt-in: consumers that want the Jackson 3 serializers / JsonHandler must
- // bring Jackson 3 onto their own classpath. Kept compileOnly here so it stays off the default
- // (Jackson 2) runtime path.
- compileOnly(libs.jackson3.databind)
- compileOnly(libs.jackson3.module.kotlin)
+ // Jackson 2 support is opt-in: consumers that want the Jackson 2 serializers / JsonHandler must
+ // bring Jackson 2 onto their own classpath (e.g. via spring-boot-jackson2). Kept compileOnly so
+ // it
+ // stays off the default (Jackson 3) runtime path.
+ compileOnly("com.fasterxml.jackson.core:jackson-databind")
+ compileOnly("com.fasterxml.jackson.module:jackson-module-kotlin")
+ compileOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect")
@@ -40,13 +43,17 @@ dependencies {
testImplementation(libs.kotest.runner)
testImplementation(libs.fory.core)
testImplementation(libs.fory.kotlin)
- testImplementation(libs.jackson3.databind)
- testImplementation(libs.jackson3.module.kotlin)
+ // Jackson 2 is compileOnly in main; the Jackson 2 serialization/handler tests need it at runtime.
+ testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
jmh(libs.jmh.core)
jmh(libs.jmh.generator)
- jmh(libs.jackson3.databind)
- jmh(libs.jackson3.module.kotlin)
+ // JacksonSerializationBenchmark exercises both Jackson lines; Jackson 2 (compileOnly in main)
+ // must
+ // be added explicitly for the jmh runtime.
+ jmh("com.fasterxml.jackson.module:jackson-module-kotlin")
+ jmh("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}
jmh { duplicateClassesStrategy.set(DuplicatesStrategy.EXCLUDE) }
diff --git a/util/src/main/kotlin/com/caplin/integration/datasourcex/util/SimpleDataSourceFactory.kt b/util/src/main/kotlin/com/caplin/integration/datasourcex/util/SimpleDataSourceFactory.kt
index 3a15174..59aaf1c 100644
--- a/util/src/main/kotlin/com/caplin/integration/datasourcex/util/SimpleDataSourceFactory.kt
+++ b/util/src/main/kotlin/com/caplin/integration/datasourcex/util/SimpleDataSourceFactory.kt
@@ -75,14 +75,14 @@ object SimpleDataSourceFactory {
*
* @param simpleConfig The simple configuration for the data source.
* @param jsonHandler The [JsonHandler] to use for serializing and deserializing JSON payloads.
- * This defaults to the Jackson 2 [defaultJackson2JsonHandler] backed by
- * [defaultJackson2ObjectMapper].
+ * This defaults to the Jackson 3 [defaultJackson3JsonHandler] backed by
+ * [defaultJackson3ObjectMapper].
* @return The created data source.
*/
@JvmStatic
fun createDataSource(
simpleConfig: SimpleDataSourceConfig,
- jsonHandler: JsonHandler<*> = defaultJackson2JsonHandler,
+ jsonHandler: JsonHandler<*> = defaultJackson3JsonHandler,
): DataSource {
val logPath =
simpleConfig.logDirectory