Skip to content

Commit 74fa003

Browse files
author
Mark Pollack
committed
Declare and check a supported Jackson floor
acp-json-jackson2 needs Jackson 2.18.1 or later and acp-json-jackson3 Jackson 3.0.0 or later: the oldest releases the SDK's tests pass with. Each supplier and mapper constructor checks jackson-core and jackson-databind and fails with a message naming the found and required versions, instead of a NoSuchMethodError on the first message.
1 parent b70c050 commit 74fa003

10 files changed

Lines changed: 254 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
305305

306306
### Changed
307307

308+
- **A supported Jackson floor, checked at startup: Jackson 2.18.1 or later for
309+
`acp-json-jackson2`, Jackson 3.0.0 or later for `acp-json-jackson3`.** Frameworks manage their
310+
own Jackson version, and the SDK stated none: an older Jackson (Vert.x 4.5's jackson-core 2.16.1
311+
under jackson-databind 2.22.3, for example) failed much later with a `NoSuchMethodError` while
312+
reading a message. Each mapper supplier (`JacksonAcpJsonMapperSupplier`,
313+
`Jackson3AcpJsonMapperSupplier`) and mapper constructor now checks the jackson-core and
314+
jackson-databind on the classpath and fails with an `IllegalStateException` naming the version
315+
found and the version required. The floors are the oldest releases the SDK's tests pass with
316+
(2.18.0 drops unknown values of the open enumerations, and 2.17 the unknown fields the SDK keeps
317+
for forward compatibility). Quarkus 3.40 (Jackson 2.21.7) and Spring Boot 4.1 (Jackson 3.1.7) are
318+
within them. Migration: none for a
319+
consistent Jackson at or above the floor; otherwise align the Jackson artifacts on one version,
320+
for example by importing the `jackson-bom`.
321+
308322
- **Breaking: `StdioAcpClientTransport.awaitForExit()` is renamed `awaitProcessExit()`**, so it does
309323
not sit beside `awaitTermination()` (which completes when the transport ends, not the process).
310324
Interrupted, it now throws `CancellationException` and keeps the thread's interrupt flag; it used

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ Jackson 2 (-200), and an application's own supplier (default 0) wins over both.
8686
the system property `acp.json.mapper.supplier` to the supplier's class name, for example
8787
`com.agentclientprotocol.sdk.json.JacksonAcpJsonMapperSupplier`.
8888

89+
**Supported Jackson versions.** `acp-json-jackson2` needs Jackson **2.18.1 or later** and
90+
`acp-json-jackson3` needs Jackson **3.0.0 or later**, for jackson-core and jackson-databind alike;
91+
the SDK is built and released against the versions in its POM (Jackson 2.22.3 and 3.1.7).
92+
Frameworks manage their own Jackson version: Quarkus 3.40 (Jackson 2.21.7) and Spring Boot 4.1
93+
(Jackson 3.1.7) are within these floors. Creating a mapper checks the versions on the classpath and
94+
fails at once with an `IllegalStateException` that names the version found and the one required,
95+
instead of a `NoSuchMethodError` on the first message. Keep the Jackson artifacts on one version,
96+
for example by importing `com.fasterxml.jackson:jackson-bom` (or `tools.jackson:jackson-bom`).
97+
8998
For annotation-based agent development:
9099
```xml
91100
<dependency>

‎acp-json-jackson2/src/main/java/com/agentclientprotocol/sdk/json/JacksonAcpJsonMapper.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
* Jackson 2 implementation of {@link AcpJsonMapper}, shipped in {@code acp-json-jackson2}.
2727
* Wraps a Jackson {@link ObjectMapper} but keeps the SDK decoupled from Jackson at the API
2828
* level. It keeps the package it had when it lived in {@code acp-core}, so code that
29-
* constructs it compiles unchanged.
29+
* constructs it compiles unchanged. It needs Jackson 2.18.1 or later (jackson-core and
30+
* jackson-databind), which it checks when it is created.
3031
*
3132
* @author Mark Pollack
3233
*/
@@ -93,8 +94,11 @@ public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p,
9394
* Constructs a new JacksonAcpJsonMapper with the given ObjectMapper.
9495
* @param objectMapper the ObjectMapper to use. Must not be null.
9596
* @throws IllegalArgumentException if the provided ObjectMapper is null.
97+
* @throws IllegalStateException if the Jackson 2 on the classpath is older than the supported
98+
* floor (see the type comment)
9699
*/
97100
public JacksonAcpJsonMapper(ObjectMapper objectMapper) {
101+
JacksonVersions.requireSupported();
98102
if (objectMapper == null) {
99103
throw new IllegalArgumentException("ObjectMapper must not be null");
100104
}

‎acp-json-jackson2/src/main/java/com/agentclientprotocol/sdk/json/JacksonAcpJsonMapperSupplier.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public int priority() {
2323

2424
@Override
2525
public AcpJsonMapper get() {
26+
// Before building the ObjectMapper: an unsupported Jackson may fail inside it.
27+
JacksonVersions.requireSupported();
2628
return new JacksonAcpJsonMapper(JacksonAcpJsonMapper.defaultObjectMapper());
2729
}
2830

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.json;
6+
7+
import com.fasterxml.jackson.core.Version;
8+
9+
/**
10+
* The Jackson 2 versions acp-json-jackson2 supports: {@value #FLOOR} or later, for jackson-core
11+
* and jackson-databind alike. Frameworks manage their own Jackson version, and an older
12+
* jackson-core under a newer jackson-databind fails much later with a {@code NoSuchMethodError}
13+
* while reading a message; checking when the mapper is created names the problem instead.
14+
*/
15+
final class JacksonVersions {
16+
17+
/**
18+
* The oldest Jackson 2 release the SDK's tests pass with. 2.18.0 loses unknown values of the
19+
* SDK's open enumerations (stop reasons, plan entry statuses), and 2.17 the unknown fields the
20+
* SDK keeps for forward compatibility.
21+
*/
22+
static final String FLOOR = "2.18.1";
23+
24+
private static final Version FLOOR_VERSION = new Version(2, 18, 1, null, "com.fasterxml.jackson.core",
25+
"jackson-core");
26+
27+
private JacksonVersions() {
28+
}
29+
30+
/**
31+
* Fails unless the jackson-core and jackson-databind on the classpath are {@value #FLOOR} or
32+
* later.
33+
* @throws IllegalStateException naming the version found and the version required
34+
*/
35+
static void requireSupported() {
36+
require("jackson-core", com.fasterxml.jackson.core.json.PackageVersion.VERSION);
37+
require("jackson-databind", com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION);
38+
}
39+
40+
/**
41+
* Fails unless {@code found} is {@value #FLOOR} or later.
42+
* @param artifact the Jackson artifact the version belongs to
43+
* @param found the version on the classpath
44+
* @throws IllegalStateException naming the version found and the version required
45+
*/
46+
static void require(String artifact, Version found) {
47+
if (found.getMajorVersion() != FLOOR_VERSION.getMajorVersion()
48+
|| compare(found, FLOOR_VERSION) < 0) {
49+
throw new IllegalStateException("acp-json-jackson2 needs Jackson " + FLOOR + " or later, but " + artifact
50+
+ " " + found.toString() + " is on the classpath (jackson-core "
51+
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION.toString() + ", jackson-databind "
52+
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION.toString()
53+
+ "). Align the Jackson 2 artifacts on one version, " + FLOOR
54+
+ " or later, for example by importing com.fasterxml.jackson:jackson-bom.");
55+
}
56+
}
57+
58+
/** Compares major, minor and patch level only, ignoring the artifact and any snapshot. */
59+
private static int compare(Version a, Version b) {
60+
int major = Integer.compare(a.getMajorVersion(), b.getMajorVersion());
61+
if (major != 0) {
62+
return major;
63+
}
64+
int minor = Integer.compare(a.getMinorVersion(), b.getMinorVersion());
65+
return (minor != 0) ? minor : Integer.compare(a.getPatchLevel(), b.getPatchLevel());
66+
}
67+
68+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.json;
6+
7+
import com.fasterxml.jackson.core.Version;
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.assertj.core.api.Assertions.assertThatCode;
11+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
12+
13+
/**
14+
* acp-json-jackson2 states its Jackson floor and checks it when a mapper is created: an older
15+
* jackson-core (a framework's BOM pinning 2.16.1 under databind 2.22.3, say) fails at once with
16+
* a message naming both versions, instead of a {@code NoSuchMethodError} on the first message.
17+
*/
18+
class JacksonVersionsTest {
19+
20+
@Test
21+
void aJacksonOlderThanTheFloorIsRefusedNamingFoundAndRequired() {
22+
assertThatThrownBy(() -> JacksonVersions.require("jackson-core",
23+
new Version(2, 16, 1, null, "com.fasterxml.jackson.core", "jackson-core")))
24+
.isInstanceOf(IllegalStateException.class)
25+
.hasMessageContaining("jackson-core 2.16.1")
26+
.hasMessageContaining("needs Jackson " + JacksonVersions.FLOOR + " or later");
27+
}
28+
29+
@Test
30+
void theFloorAndTheJacksonOfQuarkusAreAccepted() {
31+
assertThatCode(() -> JacksonVersions.require("jackson-databind",
32+
new Version(2, 18, 1, null, "com.fasterxml.jackson.core", "jackson-databind")))
33+
.doesNotThrowAnyException();
34+
assertThatThrownBy(() -> JacksonVersions.require("jackson-databind",
35+
new Version(2, 18, 0, null, "com.fasterxml.jackson.core", "jackson-databind")))
36+
.isInstanceOf(IllegalStateException.class);
37+
// Quarkus 3.40 manages Jackson 2.21.7.
38+
assertThatCode(() -> JacksonVersions.require("jackson-databind",
39+
new Version(2, 21, 7, null, "com.fasterxml.jackson.core", "jackson-databind")))
40+
.doesNotThrowAnyException();
41+
}
42+
43+
@Test
44+
void theJacksonOnTheClasspathIsSupportedAndTheSupplierChecksIt() {
45+
assertThatCode(JacksonVersions::requireSupported).doesNotThrowAnyException();
46+
assertThatCode(() -> new JacksonAcpJsonMapperSupplier().get()).doesNotThrowAnyException();
47+
}
48+
49+
}

‎acp-json-jackson3/src/main/java/com/agentclientprotocol/sdk/json/jackson3/Jackson3AcpJsonMapper.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
/**
2929
* Jackson 3 ({@code tools.jackson}) implementation of {@link AcpJsonMapper}, shipped in
3030
* {@code acp-json-jackson3}. Wraps a Jackson {@link JsonMapper} but keeps the SDK
31-
* decoupled from Jackson at the API level.
31+
* decoupled from Jackson at the API level. It needs Jackson 3.0.0 or later (jackson-core and
32+
* jackson-databind), which it checks when it is created.
3233
*
3334
* <p>
3435
* The schema records carry Jackson 2 annotations ({@code com.fasterxml.jackson.annotation}),
@@ -131,8 +132,11 @@ public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p,
131132
* Constructs a new Jackson3AcpJsonMapper with the given JsonMapper, used as is.
132133
* @param jsonMapper the JsonMapper to use. Must not be null.
133134
* @throws IllegalArgumentException if the provided JsonMapper is null.
135+
* @throws IllegalStateException if the Jackson 3 on the classpath is older than the supported
136+
* floor (see the type comment)
134137
*/
135138
public Jackson3AcpJsonMapper(JsonMapper jsonMapper) {
139+
Jackson3Versions.requireSupported();
136140
if (jsonMapper == null) {
137141
throw new IllegalArgumentException("JsonMapper must not be null");
138142
}

‎acp-json-jackson3/src/main/java/com/agentclientprotocol/sdk/json/jackson3/Jackson3AcpJsonMapperSupplier.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public int priority() {
2828

2929
@Override
3030
public AcpJsonMapper get() {
31+
// Before building the JsonMapper: an unsupported Jackson may fail inside it.
32+
Jackson3Versions.requireSupported();
3133
return new Jackson3AcpJsonMapper(Jackson3AcpJsonMapper.defaultJsonMapper());
3234
}
3335

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.json.jackson3;
6+
7+
import tools.jackson.core.Version;
8+
9+
/**
10+
* The Jackson 3 versions acp-json-jackson3 supports: {@value #FLOOR} or later, for jackson-core
11+
* and jackson-databind alike. Checked when the mapper is created, so a framework that manages an
12+
* older Jackson fails at once with a message naming the versions, not later while reading a
13+
* message.
14+
*/
15+
final class Jackson3Versions {
16+
17+
/** The oldest Jackson 3 release the SDK's tests pass with: the first release of Jackson 3. */
18+
static final String FLOOR = "3.0.0";
19+
20+
private static final int FLOOR_MAJOR = 3;
21+
22+
private static final int FLOOR_MINOR = 0;
23+
24+
private Jackson3Versions() {
25+
}
26+
27+
/**
28+
* Fails unless the jackson-core and jackson-databind on the classpath are {@value #FLOOR} or
29+
* later.
30+
* @throws IllegalStateException naming the version found and the version required
31+
*/
32+
static void requireSupported() {
33+
require("jackson-core", tools.jackson.core.json.PackageVersion.VERSION);
34+
require("jackson-databind", tools.jackson.databind.cfg.PackageVersion.VERSION);
35+
}
36+
37+
/**
38+
* Fails unless {@code found} is {@value #FLOOR} or later.
39+
* @param artifact the Jackson artifact the version belongs to
40+
* @param found the version on the classpath
41+
* @throws IllegalStateException naming the version found and the version required
42+
*/
43+
static void require(String artifact, Version found) {
44+
if (found.getMajorVersion() != FLOOR_MAJOR || found.getMinorVersion() < FLOOR_MINOR) {
45+
throw new IllegalStateException("acp-json-jackson3 needs Jackson " + FLOOR + " or later, but " + artifact
46+
+ " " + found.toString() + " is on the classpath (jackson-core "
47+
+ tools.jackson.core.json.PackageVersion.VERSION.toString() + ", jackson-databind "
48+
+ tools.jackson.databind.cfg.PackageVersion.VERSION.toString()
49+
+ "). Align the Jackson 3 artifacts on one version, " + FLOOR
50+
+ " or later, for example by importing tools.jackson:jackson-bom.");
51+
}
52+
}
53+
54+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package com.agentclientprotocol.sdk.json.jackson3;
6+
7+
import tools.jackson.core.Version;
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.assertj.core.api.Assertions.assertThatCode;
11+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
12+
13+
/**
14+
* acp-json-jackson3 states its Jackson 3 floor and checks it when a mapper is created: a Jackson
15+
* outside it fails at once with a message naming the versions found and required.
16+
*/
17+
class Jackson3VersionsTest {
18+
19+
@Test
20+
void aJacksonOutsideTheFloorIsRefusedNamingFoundAndRequired() {
21+
assertThatThrownBy(() -> Jackson3Versions.require("jackson-core",
22+
new Version(2, 22, 3, null, "tools.jackson.core", "jackson-core")))
23+
.isInstanceOf(IllegalStateException.class)
24+
.hasMessageContaining("jackson-core 2.22.3")
25+
.hasMessageContaining("needs Jackson " + Jackson3Versions.FLOOR + " or later");
26+
}
27+
28+
@Test
29+
void theFloorAndTheJacksonOfSpringBootAreAccepted() {
30+
String[] floor = Jackson3Versions.FLOOR.split("\\.");
31+
assertThatCode(() -> Jackson3Versions.require("jackson-databind", new Version(3, Integer.parseInt(floor[1]),
32+
0, null, "tools.jackson.core", "jackson-databind")))
33+
.doesNotThrowAnyException();
34+
// Spring Boot 4.1 manages Jackson 3.1.7.
35+
assertThatCode(() -> Jackson3Versions.require("jackson-databind",
36+
new Version(3, 1, 7, null, "tools.jackson.core", "jackson-databind")))
37+
.doesNotThrowAnyException();
38+
}
39+
40+
@Test
41+
void theJacksonOnTheClasspathIsSupportedAndTheSupplierChecksIt() {
42+
assertThatCode(Jackson3Versions::requireSupported).doesNotThrowAnyException();
43+
assertThatCode(() -> new Jackson3AcpJsonMapperSupplier().get()).doesNotThrowAnyException();
44+
}
45+
46+
}

0 commit comments

Comments
 (0)