Please do a quick search on GitHub issues first, there might be already a duplicate issue for the one you are about to create.
If the bug is trivial, just go ahead and create the issue. Otherwise, please take a few moments and fill in the following sections:
Bug description
Several request records in io.modelcontextprotocol.spec.McpSchema cannot be deserialized correctly by the Jackson 3 mapper (mcp-json-jackson3). Required fields silently bind to null, which then causes a NullPointerException in the stateless transports when they read those fields (e.g. WebMvcStatelessServerTransport calling request.name().equals(...)).
Affected records (all in McpSchema):
| Record |
Required fields that silently bind to null |
CallToolRequest |
name |
InitializeRequest |
protocolVersion, capabilities, clientInfo |
GetPromptRequest |
name |
ReadResourceRequest |
uri |
Each of these records declares three candidate creators:
- The canonical record constructor (parameters carry
@JsonProperty).
- One or more deprecated alternate constructors with no Jackson annotations.
- A package-private
@JsonCreator-annotated static fromJson(...) factory that defaults missing required fields to "" and logs a warning.
With Jackson 2, the static fromJson factory is selected as the creator: @JsonProperty bindings on its parameters are applied, and if a required field is missing from the JSON, fromJson substitutes "" and logs a warning. With Jackson 3, creator resolution does not route through fromJson; the canonical record constructor is used directly, but the @JsonProperty parameter bindings are not applied, so name / uri / etc. end up null even when present in the payload.
Root cause: Jackson 3 tightened record creator detection. For records, the canonical constructor is treated as an implicit PROPERTIES-mode creator and is preferred; an @JsonCreator on a package-private static factory no longer reliably overrides it, and the unannotated deprecated alternate constructors add further ambiguity.
The resulting failure surfaces as e.g.:
java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because the return value of
"io.modelcontextprotocol.spec.McpSchema$CallToolRequest.name()" is null
at io.modelcontextprotocol.server.transport.WebMvcStatelessServerTransport...
Proposed fix: for each affected record, collapse to a single, unambiguous canonical creator: declare the canonical constructor explicitly, annotate it @JsonCreator, fold the null→"" defaulting from fromJson into it, and remove both the fromJson factory and the unannotated deprecated alternate constructors.
Environment
io.modelcontextprotocol.sdk:mcp-core: 2.0.0-M2 / 2.0.0-M3 (current main at 2.0.0-SNAPSHOT has the same shape)
io.modelcontextprotocol.sdk:mcp-json-jackson3: 2.0.0-M2 and later
- Jackson 3.x (
tools.jackson.databind)
- Java 21
- Spring AI
spring-ai-starter-mcp-server on Spring Boot 3.x (also reproduces standalone: Spring AI is not required)
- Transport: any of the stateless transports (
WebMvcStatelessServerTransport, etc.); surfaces wherever these records are deserialized
Steps to reproduce
- Add
mcp-core and mcp-json-jackson3 to the classpath (so ServiceLoader picks the Jackson 3 supplier).
- Build a
JacksonMcpJsonMapper wrapping a Jackson 3 JsonMapper.
- Deserialize a valid
tools/call JSON payload into McpSchema.CallToolRequest.
- Observe that
req.name() returns null even though the payload set "name": "...".
End-to-end variant: deploy any Spring AI MCP stateless server that picks up mcp-json-jackson3 via ServiceLoader, POST a tools/call request, observe the NPE in WebMvcStatelessServerTransport.
Expected behavior
Deserialization should bind name (and the other required fields on the other affected records) from the JSON payload, just as the Jackson 2 mapper does. With well-formed input, req.name() should equal the value of the "name" field in the JSON. With missing required fields, the documented fromJson defaulting behavior (default to "", log a warning) should apply, and the same outcome should be produced by both the Jackson 2 and Jackson 3 mappers.
Minimal Complete Reproducible example
import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
import static org.assertj.core.api.Assertions.assertThat;
class CallToolRequestJackson3Test {
@Test
void name_bindsFromJson() {
var mapper = new JacksonMcpJsonMapper(new JsonMapper());
var req = mapper.readValue(
"""
{"name": "search_tool", "arguments": {"x": 1}, "_meta": {}}
""",
McpSchema.CallToolRequest.class);
// Expected: "search_tool"
// Actual: null -> downstream WebMvcStatelessServerTransport NPEs on name().equals(...)
assertThat(req.name()).isEqualTo("search_tool");
}
}
The same failure shape reproduces for InitializeRequest (protocolVersion / capabilities / clientInfo), GetPromptRequest (name), and ReadResourceRequest (uri).
Please do a quick search on GitHub issues first, there might be already a duplicate issue for the one you are about to create.
If the bug is trivial, just go ahead and create the issue. Otherwise, please take a few moments and fill in the following sections:
Bug description
Several request records in
io.modelcontextprotocol.spec.McpSchemacannot be deserialized correctly by the Jackson 3 mapper (mcp-json-jackson3). Required fields silently bind tonull, which then causes aNullPointerExceptionin the stateless transports when they read those fields (e.g.WebMvcStatelessServerTransportcallingrequest.name().equals(...)).Affected records (all in
McpSchema):nullCallToolRequestnameInitializeRequestprotocolVersion,capabilities,clientInfoGetPromptRequestnameReadResourceRequesturiEach of these records declares three candidate creators:
@JsonProperty).@JsonCreator-annotated staticfromJson(...)factory that defaults missing required fields to""and logs a warning.With Jackson 2, the static
fromJsonfactory is selected as the creator:@JsonPropertybindings on its parameters are applied, and if a required field is missing from the JSON,fromJsonsubstitutes""and logs a warning. With Jackson 3, creator resolution does not route throughfromJson; the canonical record constructor is used directly, but the@JsonPropertyparameter bindings are not applied, soname/uri/ etc. end upnulleven when present in the payload.Root cause: Jackson 3 tightened record creator detection. For records, the canonical constructor is treated as an implicit
PROPERTIES-mode creator and is preferred; an@JsonCreatoron a package-private static factory no longer reliably overrides it, and the unannotated deprecated alternate constructors add further ambiguity.The resulting failure surfaces as e.g.:
Proposed fix: for each affected record, collapse to a single, unambiguous canonical creator: declare the canonical constructor explicitly, annotate it
@JsonCreator, fold the null→""defaulting fromfromJsoninto it, and remove both thefromJsonfactory and the unannotated deprecated alternate constructors.Environment
io.modelcontextprotocol.sdk:mcp-core:2.0.0-M2/2.0.0-M3(currentmainat2.0.0-SNAPSHOThas the same shape)io.modelcontextprotocol.sdk:mcp-json-jackson3:2.0.0-M2and latertools.jackson.databind)spring-ai-starter-mcp-serveron Spring Boot 3.x (also reproduces standalone: Spring AI is not required)WebMvcStatelessServerTransport, etc.); surfaces wherever these records are deserializedSteps to reproduce
mcp-coreandmcp-json-jackson3to the classpath (soServiceLoaderpicks the Jackson 3 supplier).JacksonMcpJsonMapperwrapping a Jackson 3JsonMapper.tools/callJSON payload intoMcpSchema.CallToolRequest.req.name()returnsnulleven though the payload set"name": "...".End-to-end variant: deploy any Spring AI MCP stateless server that picks up
mcp-json-jackson3viaServiceLoader, POST atools/callrequest, observe the NPE inWebMvcStatelessServerTransport.Expected behavior
Deserialization should bind
name(and the other required fields on the other affected records) from the JSON payload, just as the Jackson 2 mapper does. With well-formed input,req.name()should equal the value of the"name"field in the JSON. With missing required fields, the documentedfromJsondefaulting behavior (default to"", log a warning) should apply, and the same outcome should be produced by both the Jackson 2 and Jackson 3 mappers.Minimal Complete Reproducible example
The same failure shape reproduces for
InitializeRequest(protocolVersion/capabilities/clientInfo),GetPromptRequest(name), andReadResourceRequest(uri).