com.networknt
json-schema-validator
diff --git a/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
index 0e79c3e0e..c0113c50c 100644
--- a/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
+++ b/mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/jackson/JacksonMcpJsonMapperSupplier.java
@@ -4,6 +4,10 @@
package io.modelcontextprotocol.json.jackson;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
@@ -12,7 +16,7 @@
* serialization and deserialization.
*
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
- * {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ * {@link ObjectMapper} configured for JPMS (Java Platform Module System) compatibility.
*/
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
@@ -20,13 +24,33 @@ public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
* JSON serialization and deserialization.
*
- * The returned {@link McpJsonMapper} is backed by a new instance of
- * {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ * The returned {@link McpJsonMapper} is backed by a JPMS-compatible
+ * {@link ObjectMapper} that does not require {@code --add-opens} JVM flags.
* @return a new {@link McpJsonMapper} instance
*/
@Override
public McpJsonMapper get() {
- return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
+ return new JacksonMcpJsonMapper(createJpmsCompatibleMapper());
+ }
+
+ /**
+ * Creates an ObjectMapper configured for JPMS compatibility.
+ *
+ * The mapper is configured to:
+ *
+ * - Not call {@code setAccessible()} on constructors/fields, avoiding the need for
+ * {@code --add-opens} flags
+ * - Use the {@link ParameterNamesModule} to discover constructor parameter names
+ * from bytecode (requires {@code -parameters} compiler flag, which is already
+ * configured in the parent pom.xml)
+ *
+ * @return a JPMS-compatible ObjectMapper
+ */
+ private static ObjectMapper createJpmsCompatibleMapper() {
+ return JsonMapper.builder()
+ .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
+ .addModule(new ParameterNamesModule())
+ .build();
}
}
diff --git a/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson/JpmsCompatibilityTests.java b/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson/JpmsCompatibilityTests.java
new file mode 100644
index 000000000..644294532
--- /dev/null
+++ b/mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/jackson/JpmsCompatibilityTests.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2025 - 2025 the original author or authors.
+ */
+
+package io.modelcontextprotocol.json.jackson;
+
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+
+/**
+ * Tests verifying JPMS (Java Platform Module System) compatibility.
+ *
+ * These tests ensure that JSON deserialization of Java records works without requiring
+ * {@code --add-opens} JVM flags.
+ */
+public class JpmsCompatibilityTests {
+
+ private McpJsonMapper jsonMapper;
+
+ // Test records must be public for JPMS-compatible Jackson to access them
+ public record SimpleRecord(String name, String description) {
+ }
+
+ public record RecordWithMap(String type, Map properties) {
+ }
+
+ public record RecordWithList(List items, boolean enabled) {
+ }
+
+ public record NestedRecord(String id, SimpleRecord nested) {
+ }
+
+ @BeforeEach
+ void setUp() {
+ jsonMapper = new JacksonMcpJsonMapperSupplier().get();
+ }
+
+ @Test
+ @DisplayName("Should deserialize simple record without reflection access")
+ void deserializeSimpleRecord() throws Exception {
+ String json = """
+ {
+ "name": "test-name",
+ "description": "A test description"
+ }
+ """;
+
+ assertThatNoException().isThrownBy(() -> {
+ SimpleRecord record = jsonMapper.readValue(json, SimpleRecord.class);
+ assertThat(record.name()).isEqualTo("test-name");
+ assertThat(record.description()).isEqualTo("A test description");
+ });
+ }
+
+ @Test
+ @DisplayName("Should deserialize record with map without reflection access")
+ void deserializeRecordWithMap() throws Exception {
+ String json = """
+ {
+ "type": "object",
+ "properties": {
+ "key1": "value1",
+ "key2": 42
+ }
+ }
+ """;
+
+ assertThatNoException().isThrownBy(() -> {
+ RecordWithMap record = jsonMapper.readValue(json, RecordWithMap.class);
+ assertThat(record.type()).isEqualTo("object");
+ assertThat(record.properties()).containsKey("key1");
+ });
+ }
+
+ @Test
+ @DisplayName("Should deserialize record with list without reflection access")
+ void deserializeRecordWithList() throws Exception {
+ String json = """
+ {
+ "items": ["a", "b", "c"],
+ "enabled": true
+ }
+ """;
+
+ assertThatNoException().isThrownBy(() -> {
+ RecordWithList record = jsonMapper.readValue(json, RecordWithList.class);
+ assertThat(record.enabled()).isTrue();
+ assertThat(record.items()).containsExactly("a", "b", "c");
+ });
+ }
+
+ @Test
+ @DisplayName("Should deserialize nested records without reflection access")
+ void deserializeNestedRecord() throws Exception {
+ String json = """
+ {
+ "id": "outer-id",
+ "nested": {
+ "name": "inner-name",
+ "description": "inner-description"
+ }
+ }
+ """;
+
+ assertThatNoException().isThrownBy(() -> {
+ NestedRecord record = jsonMapper.readValue(json, NestedRecord.class);
+ assertThat(record.id()).isEqualTo("outer-id");
+ assertThat(record.nested().name()).isEqualTo("inner-name");
+ });
+ }
+
+ @Test
+ @DisplayName("Should serialize and deserialize records round-trip")
+ void roundTripSerialization() throws Exception {
+ SimpleRecord original = new SimpleRecord("my-name", "my-description");
+
+ String json = jsonMapper.writeValueAsString(original);
+ SimpleRecord deserialized = jsonMapper.readValue(json, SimpleRecord.class);
+
+ assertThat(deserialized.name()).isEqualTo(original.name());
+ assertThat(deserialized.description()).isEqualTo(original.description());
+ }
+
+ @Test
+ @DisplayName("ObjectMapper should have JPMS-compatible configuration")
+ void verifyJpmsConfiguration() {
+ JacksonMcpJsonMapper jacksonMapper = (JacksonMcpJsonMapper) jsonMapper;
+ ObjectMapper objectMapper = jacksonMapper.getObjectMapper();
+
+ // Verify CAN_OVERRIDE_ACCESS_MODIFIERS is disabled
+ assertThat(objectMapper.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS))
+ .as("CAN_OVERRIDE_ACCESS_MODIFIERS should be disabled for JPMS compatibility")
+ .isFalse();
+ }
+
+}