Skip to content

Commit ee6444c

Browse files
committed
feat: Add JPMS compatibility to Jackson 2 JSON mapper
Configure ObjectMapper with JPMS-compatible settings: - Disable CAN_OVERRIDE_ACCESS_MODIFIERS to prevent setAccessible() calls - Add ParameterNamesModule to discover constructor parameter names from bytecode instead of reflection This allows applications using the MCP SDK to operate without `--add-opens` JVM flags, enabling full JPMS module encapsulation. The SDK already compiles with `-parameters` flag, which is required for ParameterNamesModule to function. Signed-off-by: Nicholas Walter Knize <nknize@apache.org>
1 parent 2456a0e commit ee6444c

File tree

3 files changed

+180
-4
lines changed

3 files changed

+180
-4
lines changed

mcp-json-jackson2/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
<artifactId>jackson-databind</artifactId>
4545
<version>${jackson2.version}</version>
4646
</dependency>
47+
<dependency>
48+
<groupId>com.fasterxml.jackson.module</groupId>
49+
<artifactId>jackson-module-parameter-names</artifactId>
50+
<version>${jackson2.version}</version>
51+
</dependency>
4752
<dependency>
4853
<groupId>com.networknt</groupId>
4954
<artifactId>json-schema-validator</artifactId>

mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson2/JacksonMcpJsonMapperSupplier.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
package io.modelcontextprotocol.json.jackson2;
66

7+
import com.fasterxml.jackson.databind.MapperFeature;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.json.JsonMapper;
10+
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
11+
712
import io.modelcontextprotocol.json.McpJsonMapper;
813
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
914

@@ -12,21 +17,41 @@
1217
* serialization and deserialization.
1318
* <p>
1419
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
15-
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
20+
* {@link ObjectMapper} configured for JPMS (Java Platform Module System) compatibility.
1621
*/
1722
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
1823

1924
/**
2025
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
2126
* JSON serialization and deserialization.
2227
* <p>
23-
* The returned {@link McpJsonMapper} is backed by a new instance of
24-
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
28+
* The returned {@link McpJsonMapper} is backed by a JPMS-compatible
29+
* {@link ObjectMapper} that does not require {@code --add-opens} JVM flags.
2530
* @return a new {@link McpJsonMapper} instance
2631
*/
2732
@Override
2833
public McpJsonMapper get() {
29-
return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
34+
return new JacksonMcpJsonMapper(createJpmsCompatibleMapper());
35+
}
36+
37+
/**
38+
* Creates an ObjectMapper configured for JPMS compatibility.
39+
* <p>
40+
* The mapper is configured to:
41+
* <ul>
42+
* <li>Not call {@code setAccessible()} on constructors/fields, avoiding the need for
43+
* {@code --add-opens} flags</li>
44+
* <li>Use the {@link ParameterNamesModule} to discover constructor parameter names
45+
* from bytecode (requires {@code -parameters} compiler flag, which is already
46+
* configured in the parent pom.xml)</li>
47+
* </ul>
48+
* @return a JPMS-compatible ObjectMapper
49+
*/
50+
private static ObjectMapper createJpmsCompatibleMapper() {
51+
return JsonMapper.builder()
52+
.disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
53+
.addModule(new ParameterNamesModule())
54+
.build();
3055
}
3156

3257
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2026 - 2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.jackson2;
6+
7+
import com.fasterxml.jackson.databind.MapperFeature;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import io.modelcontextprotocol.json.McpJsonMapper;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.DisplayName;
12+
import org.junit.jupiter.api.Test;
13+
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatNoException;
19+
20+
/**
21+
* Tests verifying JPMS (Java Platform Module System) compatibility.
22+
* <p>
23+
* These tests ensure that JSON deserialization of Java records works without requiring
24+
* {@code --add-opens} JVM flags.
25+
*/
26+
public class JpmsCompatibilityTests {
27+
28+
private McpJsonMapper jsonMapper;
29+
30+
// Test records must be public for JPMS-compatible Jackson to access them
31+
public record SimpleRecord(String name, String description) {
32+
}
33+
34+
public record RecordWithMap(String type, Map<String, Object> properties) {
35+
}
36+
37+
public record RecordWithList(List<String> items, boolean enabled) {
38+
}
39+
40+
public record NestedRecord(String id, SimpleRecord nested) {
41+
}
42+
43+
@BeforeEach
44+
void setUp() {
45+
jsonMapper = new JacksonMcpJsonMapperSupplier().get();
46+
}
47+
48+
@Test
49+
@DisplayName("Should deserialize simple record without reflection access")
50+
void deserializeSimpleRecord() throws Exception {
51+
String json = """
52+
{
53+
"name": "test-name",
54+
"description": "A test description"
55+
}
56+
""";
57+
58+
assertThatNoException().isThrownBy(() -> {
59+
SimpleRecord record = jsonMapper.readValue(json, SimpleRecord.class);
60+
assertThat(record.name()).isEqualTo("test-name");
61+
assertThat(record.description()).isEqualTo("A test description");
62+
});
63+
}
64+
65+
@Test
66+
@DisplayName("Should deserialize record with map without reflection access")
67+
void deserializeRecordWithMap() throws Exception {
68+
String json = """
69+
{
70+
"type": "object",
71+
"properties": {
72+
"key1": "value1",
73+
"key2": 42
74+
}
75+
}
76+
""";
77+
78+
assertThatNoException().isThrownBy(() -> {
79+
RecordWithMap record = jsonMapper.readValue(json, RecordWithMap.class);
80+
assertThat(record.type()).isEqualTo("object");
81+
assertThat(record.properties()).containsKey("key1");
82+
});
83+
}
84+
85+
@Test
86+
@DisplayName("Should deserialize record with list without reflection access")
87+
void deserializeRecordWithList() throws Exception {
88+
String json = """
89+
{
90+
"items": ["a", "b", "c"],
91+
"enabled": true
92+
}
93+
""";
94+
95+
assertThatNoException().isThrownBy(() -> {
96+
RecordWithList record = jsonMapper.readValue(json, RecordWithList.class);
97+
assertThat(record.enabled()).isTrue();
98+
assertThat(record.items()).containsExactly("a", "b", "c");
99+
});
100+
}
101+
102+
@Test
103+
@DisplayName("Should deserialize nested records without reflection access")
104+
void deserializeNestedRecord() throws Exception {
105+
String json = """
106+
{
107+
"id": "outer-id",
108+
"nested": {
109+
"name": "inner-name",
110+
"description": "inner-description"
111+
}
112+
}
113+
""";
114+
115+
assertThatNoException().isThrownBy(() -> {
116+
NestedRecord record = jsonMapper.readValue(json, NestedRecord.class);
117+
assertThat(record.id()).isEqualTo("outer-id");
118+
assertThat(record.nested().name()).isEqualTo("inner-name");
119+
});
120+
}
121+
122+
@Test
123+
@DisplayName("Should serialize and deserialize records round-trip")
124+
void roundTripSerialization() throws Exception {
125+
SimpleRecord original = new SimpleRecord("my-name", "my-description");
126+
127+
String json = jsonMapper.writeValueAsString(original);
128+
SimpleRecord deserialized = jsonMapper.readValue(json, SimpleRecord.class);
129+
130+
assertThat(deserialized.name()).isEqualTo(original.name());
131+
assertThat(deserialized.description()).isEqualTo(original.description());
132+
}
133+
134+
@Test
135+
@DisplayName("ObjectMapper should have JPMS-compatible configuration")
136+
void verifyJpmsConfiguration() {
137+
JacksonMcpJsonMapper jacksonMapper = (JacksonMcpJsonMapper) jsonMapper;
138+
ObjectMapper objectMapper = jacksonMapper.getObjectMapper();
139+
140+
// Verify CAN_OVERRIDE_ACCESS_MODIFIERS is disabled
141+
assertThat(objectMapper.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS))
142+
.as("CAN_OVERRIDE_ACCESS_MODIFIERS should be disabled for JPMS compatibility")
143+
.isFalse();
144+
}
145+
146+
}

0 commit comments

Comments
 (0)