Skip to content

Commit d93ba3d

Browse files
committed
feat: Add JPMS compatibility to Jackson 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 af65356 commit d93ba3d

File tree

3 files changed

+179
-4
lines changed

3 files changed

+179
-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>${jackson.version}</version>
4646
</dependency>
47+
<dependency>
48+
<groupId>com.fasterxml.jackson.module</groupId>
49+
<artifactId>jackson-module-parameter-names</artifactId>
50+
<version>${jackson.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/jackson/JacksonMcpJsonMapperSupplier.java

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

55
package io.modelcontextprotocol.json.jackson;
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;
711
import io.modelcontextprotocol.json.McpJsonMapper;
812
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
913

@@ -12,21 +16,41 @@
1216
* serialization and deserialization.
1317
* <p>
1418
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
15-
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
19+
* {@link ObjectMapper} configured for JPMS (Java Platform Module System) compatibility.
1620
*/
1721
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
1822

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

3256
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2025 - 2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.json.jackson;
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)