diff --git a/claude-code-sdk/pom.xml b/claude-code-sdk/pom.xml
index d5134aa9..147d139d 100644
--- a/claude-code-sdk/pom.xml
+++ b/claude-code-sdk/pom.xml
@@ -7,7 +7,7 @@
io.github.markpollack
claude-agent-sdk-parent
- 1.8.0-SNAPSHOT
+ 2.0.0-SNAPSHOT
../pom.xml
@@ -31,15 +31,7 @@
zt-exec
-
-
- com.fasterxml.jackson.core
- jackson-core
-
-
- com.fasterxml.jackson.core
- jackson-databind
-
+
com.fasterxml.jackson.core
jackson-annotations
@@ -63,16 +55,13 @@
mcp
-
+
tools.jackson.core
jackson-core
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeAsyncClient.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeAsyncClient.java
index 14bed906..e6d33993 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeAsyncClient.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeAsyncClient.java
@@ -16,7 +16,6 @@
package io.github.markpollack.claude.agent.sdk;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.TransportException;
@@ -39,6 +38,7 @@
import io.github.markpollack.claude.agent.sdk.permission.PermissionResult;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionCallback;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionContext;
+import tools.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeSyncClient.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeSyncClient.java
index 00f7c5a2..4723e3be 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeSyncClient.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/DefaultClaudeSyncClient.java
@@ -16,7 +16,6 @@
package io.github.markpollack.claude.agent.sdk;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.TransportException;
@@ -43,6 +42,7 @@
import io.github.markpollack.claude.agent.sdk.permission.PermissionResult;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionCallback;
import io.github.markpollack.claude.agent.sdk.permission.ToolPermissionContext;
+import tools.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpMessageHandler.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpMessageHandler.java
index 2e279e20..708b456d 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpMessageHandler.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpMessageHandler.java
@@ -16,12 +16,12 @@
package io.github.markpollack.claude.agent.sdk.mcp;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap;
import java.util.List;
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpServerConfig.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpServerConfig.java
index 74fdbcce..d0cad0dc 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpServerConfig.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/mcp/McpServerConfig.java
@@ -33,8 +33,8 @@
* In-process SDK servers are managed by the Java SDK and communicate via the mcp_message
* control protocol.
*/
-@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = McpServerConfig.McpStdioServerConfig.class,
- visible = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
+ defaultImpl = McpServerConfig.McpStdioServerConfig.class, visible = true)
@JsonSubTypes({ @JsonSubTypes.Type(value = McpServerConfig.McpStdioServerConfig.class, name = "stdio"),
@JsonSubTypes.Type(value = McpServerConfig.McpSseServerConfig.class, name = "sse"),
@JsonSubTypes.Type(value = McpServerConfig.McpHttpServerConfig.class, name = "http"),
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/ControlMessageParser.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/ControlMessageParser.java
index f267330e..0f3307f0 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/ControlMessageParser.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/ControlMessageParser.java
@@ -16,10 +16,6 @@
package io.github.markpollack.claude.agent.sdk.parsing;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
@@ -27,6 +23,10 @@
import io.github.markpollack.claude.agent.sdk.types.RateLimitEvent;
import io.github.markpollack.claude.agent.sdk.types.control.ControlRequest;
import io.github.markpollack.claude.agent.sdk.types.control.ControlResponse;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
/**
* Parser for Claude CLI bidirectional control protocol messages. This parser handles both
@@ -80,7 +80,7 @@ public ControlMessageParser() {
* @param maxBufferSize maximum message size in bytes (for buffer overflow protection)
*/
public ControlMessageParser(int maxBufferSize) {
- this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ this.objectMapper = new ObjectMapper();
this.messageParser = new MessageParser();
this.maxBufferSize = maxBufferSize > 0 ? maxBufferSize : DEFAULT_MAX_BUFFER_SIZE;
}
@@ -99,7 +99,7 @@ public ControlMessageParser(ObjectMapper objectMapper) {
* @param maxBufferSize maximum message size in bytes (for buffer overflow protection)
*/
public ControlMessageParser(ObjectMapper objectMapper, int maxBufferSize) {
- this.objectMapper = objectMapper.copy().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ this.objectMapper = objectMapper.rebuild().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();
this.messageParser = new MessageParser();
this.maxBufferSize = maxBufferSize > 0 ? maxBufferSize : DEFAULT_MAX_BUFFER_SIZE;
}
@@ -127,7 +127,7 @@ public ParsedMessage parse(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseFromNode(root, json);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
@@ -149,11 +149,11 @@ public int getMaxBufferSize() {
*/
public ParsedMessage parseFromNode(JsonNode node, String originalJson) throws MessageParseException {
JsonNode typeNode = node.get("type");
- if (typeNode == null || !typeNode.isTextual()) {
+ if (typeNode == null || !typeNode.isString()) {
throw new MessageParseException("Missing or invalid 'type' field in message");
}
- String type = typeNode.asText();
+ String type = typeNode.asString();
if (TYPE_CONTROL_REQUEST.equals(type)) {
return parseControlRequest(node, originalJson);
@@ -185,7 +185,7 @@ private ParsedMessage parseControlRequest(JsonNode node, String originalJson) th
return ParsedMessage.Control.of(request);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw new MessageParseException("Failed to parse control request: " + e.getMessage(), e);
}
}
@@ -205,7 +205,7 @@ private ParsedMessage parseControlResponse(JsonNode node, String originalJson) t
return ParsedMessage.ControlResponseMessage.of(response);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw new MessageParseException("Failed to parse control response: " + e.getMessage(), e);
}
}
@@ -222,7 +222,7 @@ private ParsedMessage parseRateLimitEvent(JsonNode node) throws MessageParseExce
event.rateLimitInfo() != null ? event.rateLimitInfo().resetsAt() : 0);
return ParsedMessage.RateLimitEventMessage.of(event);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw new MessageParseException("Failed to parse rate_limit_event: " + e.getMessage(), e);
}
}
@@ -255,9 +255,9 @@ public boolean isControlRequest(String json) {
try {
JsonNode root = objectMapper.readTree(json);
JsonNode typeNode = root.get("type");
- return typeNode != null && TYPE_CONTROL_REQUEST.equals(typeNode.asText());
+ return typeNode != null && TYPE_CONTROL_REQUEST.equals(typeNode.asString());
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
return false;
}
}
@@ -276,9 +276,9 @@ public String extractRequestId(String json) {
try {
JsonNode root = objectMapper.readTree(json);
JsonNode requestIdNode = root.get("request_id");
- return requestIdNode != null && requestIdNode.isTextual() ? requestIdNode.asText() : null;
+ return requestIdNode != null && requestIdNode.isString() ? requestIdNode.asString() : null;
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
return null;
}
}
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/JsonResultParser.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/JsonResultParser.java
index 4e130c4d..80f79506 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/JsonResultParser.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/JsonResultParser.java
@@ -18,11 +18,11 @@
import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
import io.github.markpollack.claude.agent.sdk.types.ResultMessage;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
@@ -84,7 +84,7 @@ public ResultMessage parseJsonResult(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseResultFromNode(root);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
@@ -152,7 +152,7 @@ private Map parseUsageMap(JsonNode usageNode) {
}
// Parse all usage fields
- usageNode.fields().forEachRemaining(entry -> {
+ usageNode.properties().forEach(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
@@ -162,19 +162,19 @@ private Map parseUsageMap(JsonNode usageNode) {
else if (value.isDouble()) {
usage.put(key, value.asDouble());
}
- else if (value.isTextual()) {
- usage.put(key, value.asText());
+ else if (value.isString()) {
+ usage.put(key, value.asString());
}
else if (value.isObject()) {
// Handle nested objects like server_tool_use
Map nestedMap = new HashMap<>();
- value.fields().forEachRemaining(nestedEntry -> {
+ value.properties().forEach(nestedEntry -> {
JsonNode nestedValue = nestedEntry.getValue();
if (nestedValue.isInt()) {
nestedMap.put(nestedEntry.getKey(), nestedValue.asInt());
}
- else if (nestedValue.isTextual()) {
- nestedMap.put(nestedEntry.getKey(), nestedValue.asText());
+ else if (nestedValue.isString()) {
+ nestedMap.put(nestedEntry.getKey(), nestedValue.asString());
}
else {
nestedMap.put(nestedEntry.getKey(), nestedValue.toString());
@@ -192,7 +192,7 @@ else if (nestedValue.isTextual()) {
private String getStringField(JsonNode node, String fieldName) {
JsonNode field = node.get(fieldName);
- return (field != null && !field.isNull()) ? field.asText() : null;
+ return (field != null && !field.isNull()) ? field.asString() : null;
}
private int getIntField(JsonNode node, String fieldName, int defaultValue) {
diff --git a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/MessageParser.java b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/MessageParser.java
index 82023a1d..ec265d4b 100644
--- a/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/MessageParser.java
+++ b/claude-code-sdk/src/main/java/io/github/markpollack/claude/agent/sdk/parsing/MessageParser.java
@@ -18,11 +18,11 @@
import io.github.markpollack.claude.agent.sdk.exceptions.MessageParseException;
import io.github.markpollack.claude.agent.sdk.types.*;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.HashMap;
@@ -51,7 +51,7 @@ public Message parseMessage(String json) throws MessageParseException {
JsonNode root = objectMapper.readTree(json);
return parseMessageFromNode(root);
}
- catch (JsonProcessingException e) {
+ catch (JacksonException e) {
throw MessageParseException.jsonDecodeError(json, e);
}
}
@@ -60,6 +60,15 @@ public Message parseMessage(String json) throws MessageParseException {
* Parses a JsonNode into a Message object.
*/
public Message parseMessageFromNode(JsonNode node) throws MessageParseException {
+ try {
+ return tryParseMessageFromNode(node);
+ }
+ catch (JacksonException e) {
+ throw new MessageParseException("Unexpected error when reading json fields: " + e.getMessage(), e);
+ }
+ }
+
+ private Message tryParseMessageFromNode(JsonNode node) throws MessageParseException {
String type = getStringField(node, "type");
if (type == null) {
throw new MessageParseException("Missing 'type' field in message");
@@ -91,8 +100,8 @@ private UserMessage parseUserMessage(JsonNode node) throws MessageParseException
throw new MessageParseException("Missing 'content' field in user message");
}
- if (contentNode.isTextual()) {
- return UserMessage.of(contentNode.asText());
+ if (contentNode.isString()) {
+ return UserMessage.of(contentNode.asString());
}
else if (contentNode.isArray()) {
List blocks = parseContentBlocks(contentNode);
@@ -222,8 +231,8 @@ private ToolResultBlock parseToolResultBlock(JsonNode node) throws MessageParseE
JsonNode contentNode = node.get("content");
Object content = null;
if (contentNode != null) {
- if (contentNode.isTextual()) {
- content = contentNode.asText();
+ if (contentNode.isString()) {
+ content = contentNode.asString();
}
else if (contentNode.isArray()) {
content = parseDataList(contentNode);
@@ -246,7 +255,7 @@ else if (content instanceof List) {
private Map parseDataMap(JsonNode node) {
Map map = new HashMap<>();
- node.fields().forEachRemaining(entry -> {
+ node.properties().forEach(entry -> {
map.put(entry.getKey(), parseJsonValue(entry.getValue()));
});
return map;
@@ -261,8 +270,8 @@ private List
-
-
- com.fasterxml.jackson
- jackson-bom
- ${jackson.version}
- pom
- import
-
-
-
+
tools.jackson
jackson-bom
- ${jackson3.version}
+ ${jackson.version}
pom
import
diff --git a/scripts/standalone-consumer-gate.sh b/scripts/standalone-consumer-gate.sh
index 8c1d96f8..a182859e 100755
--- a/scripts/standalone-consumer-gate.sh
+++ b/scripts/standalone-consumer-gate.sh
@@ -4,7 +4,7 @@
#
# Why this exists
# ---------------
-# The parent POM's — including its Jackson BOM imports — does
+# The parent POM's — including its Jackson BOM import — does
# NOT survive POM flattening (flattenMode=ossrh). An ordinary consumer that depends on
# claude-code-sdk without this project's parent and without any BOM therefore resolves
# whatever the flattened POM declares plus whatever transitives Maven picks. Released
@@ -35,11 +35,10 @@ cd "$REPO_ROOT"
GROUP_ID="io.github.markpollack"
ARTIFACT_ID="claude-code-sdk"
-# Required floors. Both are consumer-visible: Jackson 2 is declared directly by the SDK,
-# Jackson 3 arrives through mcp -> mcp-json-jackson3 and is declared directly so the
-# floor travels in the flattened POM.
-JACKSON2_FLOOR="2.21.6"
-JACKSON3_FLOOR="3.1.6"
+# Required floor. Jackson 3 is the SDK's JSON stack and is also required by
+# mcp -> mcp-json-jackson3; the SDK declares it directly so the floor travels in the
+# flattened POM. Jackson 2 (jackson-core/jackson-databind) must not reach consumers at all.
+JACKSON_FLOOR="3.1.6"
# Expected Java shape: class-file major 65 == Java 21.
EXPECTED_CLASSFILE_MAJOR="65"
@@ -169,27 +168,25 @@ check_floor() { # $1 label, $2 jar prefix, $3 floor, $4 expected-groupId marker
}
# Jackson 2 and Jackson 3 both publish a jackson-core/jackson-databind pair. Disambiguate
-# by version line rather than by file name.
+# by version line rather than by file name. Any Jackson 2 core/databind is a failure: the
+# SDK is Jackson 3 only. (jackson-annotations keeps 2.x coordinates by Jackson 3's design
+# and is not matched here.)
for prefix in jackson-core jackson-databind; do
for jar in "$CLOSURE/$prefix"-*.jar; do
[ -e "$jar" ] || continue
v="$(basename "$jar" | sed -E "s/^$prefix-(.+)\.jar$/\1/")"
case "$v" in
- 2.*) if version_ge "$v" "$JACKSON2_FLOOR"; then
- pass "com.fasterxml.jackson.core:$prefix resolved $v (floor $JACKSON2_FLOOR)"
+ 2.*) fail "com.fasterxml.jackson.core:$prefix $v is in the consumer closure; Jackson 2 must not reach consumers" ;;
+ 3.*) if version_ge "$v" "$JACKSON_FLOOR"; then
+ pass "tools.jackson.core:$prefix resolved $v (floor $JACKSON_FLOOR)"
else
- fail "com.fasterxml.jackson.core:$prefix resolved $v, below floor $JACKSON2_FLOOR"
- fi ;;
- 3.*) if version_ge "$v" "$JACKSON3_FLOOR"; then
- pass "tools.jackson.core:$prefix resolved $v (floor $JACKSON3_FLOOR)"
- else
- fail "tools.jackson.core:$prefix resolved $v, below floor $JACKSON3_FLOOR"
+ fail "tools.jackson.core:$prefix resolved $v, below floor $JACKSON_FLOOR"
fi ;;
*) fail "$prefix resolved an unexpected version line: $v" ;;
esac
done
done
-check_floor "tools.jackson.dataformat:jackson-dataformat-yaml" jackson-dataformat-yaml "$JACKSON3_FLOOR"
+check_floor "tools.jackson.dataformat:jackson-dataformat-yaml" jackson-dataformat-yaml "$JACKSON_FLOOR"
# Every Jackson 3 artifact must sit on one minor; skew across tools.jackson modules is a
# runtime hazard, not a cosmetic difference.
@@ -231,8 +228,8 @@ echo
# ---------------------------------------------------------------------------
# 7. Offline runtime smoke against the resolved closure.
#
-# This links the SDK's Jackson 2 parsing path and the Jackson 3 stack that mcp
-# supplies, on exactly the versions a consumer receives. It spawns no Claude CLI
+# This links the SDK's own parsing path and the JSON mapper mcp binds, both on
+# Jackson 3, on exactly the versions a consumer receives. It spawns no Claude CLI
# process, opens no network connection, and uses no credentials.
# ---------------------------------------------------------------------------
echo "[runtime smoke]"
@@ -258,7 +255,7 @@ public class ConsumerSmoke {
throw new IllegalStateException("expected a Java 21+ runtime, got " + feature);
}
- // Jackson 2 path: the SDK's own message parsing.
+ // SDK path: the SDK's own message parsing.
String assistant = "{\"type\":\"assistant\",\"message\":{\"id\":\"msg_1\",\"model\":\"m\","
+ "\"content\":[{\"type\":\"text\",\"text\":\"hello\"}]}}";
Message parsed = new MessageParser().parseMessage(assistant);
@@ -272,7 +269,7 @@ public class ConsumerSmoke {
throw new IllegalStateException("CLIOptions.builder() returned null");
}
- // Jackson 3 path: the JSON mapper mcp actually binds at runtime, loaded through
+ // mcp path: the JSON mapper mcp actually binds at runtime, loaded through
// its ServiceLoader SPI so core/databind must genuinely link.
McpJsonMapperSupplier supplier = ServiceLoader.load(McpJsonMapperSupplier.class)
.findFirst()