Skip to content

Commit 21330aa

Browse files
fix McpSchema deserialization issues with Jackson3 while keeping Jackson2 behavior unchanged #978
1 parent 6b71136 commit 21330aa

3 files changed

Lines changed: 407 additions & 0 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ public record InitializeRequest( // @formatter:off
377377
@JsonProperty("clientInfo") Implementation clientInfo,
378378
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
379379

380+
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
380381
public InitializeRequest {
381382
Assert.notNull(protocolVersion, "protocolVersion must not be null");
382383
Assert.notNull(capabilities, "capabilities must not be null");
@@ -1743,6 +1744,7 @@ public record ReadResourceRequest( // @formatter:off
17431744
@JsonProperty("uri") String uri,
17441745
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
17451746

1747+
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
17461748
public ReadResourceRequest {
17471749
Assert.notNull(uri, "uri must not be null");
17481750
}
@@ -2481,6 +2483,7 @@ public record GetPromptRequest( // @formatter:off
24812483
@JsonProperty("arguments") Map<String, Object> arguments,
24822484
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
24832485

2486+
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
24842487
public GetPromptRequest {
24852488
Assert.notNull(name, "name must not be null");
24862489
}
@@ -3076,6 +3079,7 @@ public record CallToolRequest( // @formatter:off
30763079
@JsonProperty("arguments") Map<String, Object> arguments,
30773080
@JsonProperty("_meta") Map<String, Object> meta) implements Request { // @formatter:on
30783081

3082+
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
30793083
public CallToolRequest {
30803084
Assert.notNull(name, "name must not be null");
30813085
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2026 - 2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.jackson2;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
import java.util.Map;
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
13+
import io.modelcontextprotocol.json.McpJsonMapper;
14+
import io.modelcontextprotocol.spec.McpSchema;
15+
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
16+
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
17+
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
18+
import io.modelcontextprotocol.spec.McpSchema.Implementation;
19+
import io.modelcontextprotocol.spec.McpSchema.InitializeRequest;
20+
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Parity tests for the Jackson 2 mapper, confirming that the canonical-constructor
26+
* {@code @JsonCreator} fix in {@link McpSchema} preserves the previous Jackson 2 behavior
27+
* (the {@code fromJson} static factory used to handle this path). With the fix, the
28+
* canonical constructor is the single creator and both mappers must produce the same
29+
* results.
30+
*/
31+
class McpSchemaRequestCreatorTests {
32+
33+
private final McpJsonMapper mapper = new JacksonMcpJsonMapper(new ObjectMapper());
34+
35+
@Test
36+
void callToolRequest_allFieldsPresent_bindsName() throws Exception {
37+
CallToolRequest req = mapper.readValue("""
38+
{"name": "search_tool", "arguments": {"q": "foo"}, "_meta": {"trace": "abc"}}
39+
""", CallToolRequest.class);
40+
41+
assertThat(req.name()).isEqualTo("search_tool");
42+
assertThat(req.arguments()).containsEntry("q", "foo");
43+
assertThat(req.meta()).containsEntry("trace", "abc");
44+
}
45+
46+
@Test
47+
void callToolRequest_missingName_defaultsToEmptyString() throws Exception {
48+
CallToolRequest req = mapper.readValue("""
49+
{"arguments": {"q": "foo"}}
50+
""", CallToolRequest.class);
51+
52+
assertThat(req.name()).isEqualTo("");
53+
}
54+
55+
@Test
56+
void callToolRequest_roundTrip_preservesAllFields() throws Exception {
57+
CallToolRequest original = new CallToolRequest("search_tool", Map.of("q", "foo"), Map.of("trace", "abc"));
58+
59+
String json = mapper.writeValueAsString(original);
60+
CallToolRequest roundTripped = mapper.readValue(json, CallToolRequest.class);
61+
62+
assertThat(roundTripped).isEqualTo(original);
63+
}
64+
65+
@Test
66+
void initializeRequest_allFieldsPresent_bindsAllRequired() throws Exception {
67+
InitializeRequest req = mapper.readValue("""
68+
{
69+
"protocolVersion": "2025-06-18",
70+
"capabilities": {},
71+
"clientInfo": {"name": "test-client", "version": "1.0"}
72+
}
73+
""", InitializeRequest.class);
74+
75+
assertThat(req.protocolVersion()).isEqualTo("2025-06-18");
76+
assertThat(req.clientInfo().name()).isEqualTo("test-client");
77+
}
78+
79+
@Test
80+
void initializeRequest_allRequiredMissing_defaultsAll() throws Exception {
81+
InitializeRequest req = mapper.readValue("{}", InitializeRequest.class);
82+
83+
assertThat(req.protocolVersion()).isEqualTo("");
84+
assertThat(req.capabilities()).isNotNull();
85+
assertThat(req.clientInfo()).isNotNull();
86+
assertThat(req.clientInfo().name()).isEqualTo("");
87+
}
88+
89+
@Test
90+
void initializeRequest_roundTrip_preservesAllFields() throws Exception {
91+
InitializeRequest original = new InitializeRequest("2025-06-18", new ClientCapabilities(null, null, null, null),
92+
new Implementation("test-client", "1.0"), Map.of("k", "v"));
93+
94+
String json = mapper.writeValueAsString(original);
95+
InitializeRequest roundTripped = mapper.readValue(json, InitializeRequest.class);
96+
97+
assertThat(roundTripped.protocolVersion()).isEqualTo(original.protocolVersion());
98+
assertThat(roundTripped.clientInfo()).isEqualTo(original.clientInfo());
99+
}
100+
101+
@Test
102+
void getPromptRequest_allFieldsPresent_bindsName() throws Exception {
103+
GetPromptRequest req = mapper.readValue("""
104+
{"name": "prompt-a", "arguments": {"x": 1}, "_meta": {"k": "v"}}
105+
""", GetPromptRequest.class);
106+
107+
assertThat(req.name()).isEqualTo("prompt-a");
108+
}
109+
110+
@Test
111+
void getPromptRequest_missingName_defaultsToEmptyString() throws Exception {
112+
GetPromptRequest req = mapper.readValue("""
113+
{"arguments": {"x": 1}}
114+
""", GetPromptRequest.class);
115+
116+
assertThat(req.name()).isEqualTo("");
117+
}
118+
119+
@Test
120+
void readResourceRequest_allFieldsPresent_bindsUri() throws Exception {
121+
ReadResourceRequest req = mapper.readValue("""
122+
{"uri": "resource://faults/123", "_meta": {"k": "v"}}
123+
""", ReadResourceRequest.class);
124+
125+
assertThat(req.uri()).isEqualTo("resource://faults/123");
126+
assertThat(req.meta()).containsEntry("k", "v");
127+
}
128+
129+
@Test
130+
void readResourceRequest_missingUri_defaultsToEmptyString() throws Exception {
131+
ReadResourceRequest req = mapper.readValue("{}", ReadResourceRequest.class);
132+
133+
assertThat(req.uri()).isEqualTo("");
134+
}
135+
136+
}

0 commit comments

Comments
 (0)