Skip to content

Commit 2ee5853

Browse files
authored
fix: allow additional properties by default per JSON Schema spec (#617)
- Remove automatic additionalProperties: false injection (JSON Schema spec compliance) - Support String input for structuredContent in validate() method - Move tests to mcp-json-jackson2 module with proper dependencies - Replace wildcard imports with explicit imports Complies with JSON Schema Test Suite: https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/15e4505/tests/draft2020-12/additionalProperties.json\#L112 Fixes #584 BREAKING CHANGE: Additional properties now allowed by default when not explicitly specified. Signed-off-by: Christian Tzolov <[email protected]>
1 parent 19a8c00 commit 2ee5853

File tree

8 files changed

+159
-24
lines changed

8 files changed

+159
-24
lines changed

mcp-core/src/test/java/io/modelcontextprotocol/spec/JSONRPCRequestMcpValidationTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
package io.modelcontextprotocol.spec;
66

77
import org.junit.jupiter.api.Test;
8-
import static org.junit.jupiter.api.Assertions.*;
8+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
912

1013
/**
1114
* Tests for MCP-specific validation of JSONRPCRequest ID requirements.

mcp-core/src/test/java/io/modelcontextprotocol/spec/McpErrorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import java.util.Map;
66

7-
import static org.junit.jupiter.api.Assertions.*;
7+
import static org.junit.jupiter.api.Assertions.assertNotNull;
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
89

910
class McpErrorTest {
1011

mcp-core/src/test/java/io/modelcontextprotocol/spec/PromptReferenceEqualsTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
88
import org.junit.jupiter.api.Test;
99

10-
import static org.junit.jupiter.api.Assertions.*;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
1113

1214
/**
1315
* Test class to verify the equals method implementation for PromptReference.

mcp-core/src/test/java/io/modelcontextprotocol/spec/json/gson/GsonMcpJsonMapperTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import java.util.List;
99
import java.util.Map;
1010

11-
import static org.junit.jupiter.api.Assertions.*;
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1215

1316
class GsonMcpJsonMapperTests {
1417

mcp-core/src/test/java/io/modelcontextprotocol/util/AssertTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import java.util.List;
1010

11-
import static org.junit.jupiter.api.Assertions.*;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
1214

1315
class AssertTests {
1416

mcp-json-jackson2/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,31 @@
4949
<artifactId>json-schema-validator</artifactId>
5050
<version>${json-schema-validator.version}</version>
5151
</dependency>
52+
53+
<dependency>
54+
<groupId>org.assertj</groupId>
55+
<artifactId>assertj-core</artifactId>
56+
<version>${assert4j.version}</version>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.junit.jupiter</groupId>
61+
<artifactId>junit-jupiter-api</artifactId>
62+
<version>${junit.version}</version>
63+
<scope>test</scope>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.junit.jupiter</groupId>
67+
<artifactId>junit-jupiter-params</artifactId>
68+
<version>${junit.version}</version>
69+
<scope>test</scope>
70+
</dependency>
71+
<dependency>
72+
<groupId>org.mockito</groupId>
73+
<artifactId>mockito-core</artifactId>
74+
<version>${mockito.version}</version>
75+
<scope>test</scope>
76+
</dependency>
77+
5278
</dependencies>
5379
</project>

mcp-json-jackson2/src/main/java/io/modelcontextprotocol/json/schema/jackson/DefaultJsonSchemaValidator.java

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@
77
import java.util.Set;
88
import java.util.concurrent.ConcurrentHashMap;
99

10-
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
11-
import org.slf4j.Logger;
12-
import org.slf4j.LoggerFactory;
13-
1410
import com.fasterxml.jackson.core.JsonProcessingException;
1511
import com.fasterxml.jackson.databind.JsonNode;
1612
import com.fasterxml.jackson.databind.ObjectMapper;
17-
import com.fasterxml.jackson.databind.node.ObjectNode;
1813
import com.networknt.schema.JsonSchema;
1914
import com.networknt.schema.JsonSchemaFactory;
2015
import com.networknt.schema.SpecVersion;
2116
import com.networknt.schema.ValidationMessage;
17+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
2220

2321
/**
2422
* Default implementation of the {@link JsonSchemaValidator} interface. This class
@@ -60,7 +58,9 @@ public ValidationResponse validate(Map<String, Object> schema, Object structured
6058

6159
try {
6260

63-
JsonNode jsonStructuredOutput = this.objectMapper.valueToTree(structuredContent);
61+
JsonNode jsonStructuredOutput = (structuredContent instanceof String)
62+
? this.objectMapper.readTree((String) structuredContent)
63+
: this.objectMapper.valueToTree(structuredContent);
6464

6565
Set<ValidationMessage> validationResult = this.getOrCreateJsonSchema(schema).validate(jsonStructuredOutput);
6666

@@ -125,17 +125,6 @@ private JsonSchema createJsonSchema(Map<String, Object> schema) throws JsonProce
125125
};
126126
}
127127

128-
// Handle additionalProperties setting
129-
if (schemaNode.isObject()) {
130-
ObjectNode objectSchemaNode = (ObjectNode) schemaNode;
131-
if (!objectSchemaNode.has("additionalProperties")) {
132-
// Clone the node before modification to avoid mutating the original
133-
objectSchemaNode = objectSchemaNode.deepCopy();
134-
objectSchemaNode.put("additionalProperties", false);
135-
schemaNode = objectSchemaNode;
136-
}
137-
}
138-
139128
return this.schemaFactory.getSchema(schemaNode);
140129
}
141130

mcp-core/src/test/java/io/modelcontextprotocol/spec/DefaultJsonSchemaValidatorTests.java renamed to mcp-json-jackson2/src/test/java/io/modelcontextprotocol/json/DefaultJsonSchemaValidatorTests.java

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright 2024-2024 the original author or authors.
33
*/
44

5-
package io.modelcontextprotocol.spec;
5+
package io.modelcontextprotocol.json;
66

77
import static org.junit.jupiter.api.Assertions.assertEquals;
88
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -13,6 +13,7 @@
1313
import static org.mockito.ArgumentMatchers.any;
1414
import static org.mockito.Mockito.when;
1515

16+
import java.util.List;
1617
import java.util.Map;
1718
import java.util.stream.Stream;
1819

@@ -64,6 +65,16 @@ private Map<String, Object> toMap(String json) {
6465
}
6566
}
6667

68+
private List<Map<String, Object>> toListMap(String json) {
69+
try {
70+
return objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {
71+
});
72+
}
73+
catch (Exception e) {
74+
throw new RuntimeException("Failed to parse JSON: " + json, e);
75+
}
76+
}
77+
6778
@Test
6879
void testDefaultConstructor() {
6980
DefaultJsonSchemaValidator defaultValidator = new DefaultJsonSchemaValidator();
@@ -198,6 +209,74 @@ void testValidateWithValidArraySchema() {
198209
assertNull(response.errorMessage());
199210
}
200211

212+
@Test
213+
void testValidateWithValidArraySchemaTopLevelArray() {
214+
String schemaJson = """
215+
{
216+
"$schema" : "https://json-schema.org/draft/2020-12/schema",
217+
"type" : "array",
218+
"items" : {
219+
"type" : "object",
220+
"properties" : {
221+
"city" : {
222+
"type" : "string"
223+
},
224+
"summary" : {
225+
"type" : "string"
226+
},
227+
"temperatureC" : {
228+
"type" : "number",
229+
"format" : "float"
230+
}
231+
},
232+
"required" : [ "city", "summary", "temperatureC" ]
233+
},
234+
"additionalProperties" : false
235+
}
236+
""";
237+
238+
String contentJson = """
239+
[
240+
{
241+
"city": "London",
242+
"summary": "Generally mild with frequent rainfall. Winters are cool and damp, summers are warm but rarely hot. Cloudy conditions are common throughout the year.",
243+
"temperatureC": 11.3
244+
},
245+
{
246+
"city": "New York",
247+
"summary": "Four distinct seasons with hot and humid summers, cold winters with snow, and mild springs and autumns. Precipitation is fairly evenly distributed throughout the year.",
248+
"temperatureC": 12.8
249+
},
250+
{
251+
"city": "San Francisco",
252+
"summary": "Mild year-round with a distinctive Mediterranean climate. Famous for summer fog, mild winters, and little temperature variation throughout the year. Very little rainfall in summer months.",
253+
"temperatureC": 14.6
254+
},
255+
{
256+
"city": "Tokyo",
257+
"summary": "Humid subtropical climate with hot, wet summers and mild winters. Experiences a rainy season in early summer and occasional typhoons in late summer to early autumn.",
258+
"temperatureC": 15.4
259+
}
260+
]
261+
""";
262+
263+
Map<String, Object> schema = toMap(schemaJson);
264+
265+
// Validate as JSON string
266+
ValidationResponse response = validator.validate(schema, contentJson);
267+
268+
assertTrue(response.valid());
269+
assertNull(response.errorMessage());
270+
271+
List<Map<String, Object>> structuredContent = toListMap(contentJson);
272+
273+
// Validate as List<Map<String, Object>>
274+
response = validator.validate(schema, structuredContent);
275+
276+
assertTrue(response.valid());
277+
assertNull(response.errorMessage());
278+
}
279+
201280
@Test
202281
void testValidateWithInvalidTypeSchema() {
203282
String schemaJson = """
@@ -266,7 +345,8 @@ void testValidateWithAdditionalPropertiesNotAllowed() {
266345
"properties": {
267346
"name": {"type": "string"}
268347
},
269-
"required": ["name"]
348+
"required": ["name"],
349+
"additionalProperties": false
270350
}
271351
""";
272352

@@ -316,6 +396,35 @@ void testValidateWithAdditionalPropertiesExplicitlyAllowed() {
316396
assertNull(response.errorMessage());
317397
}
318398

399+
@Test
400+
void testValidateWithDefaultAdditionalProperties() {
401+
String schemaJson = """
402+
{
403+
"type": "object",
404+
"properties": {
405+
"name": {"type": "string"}
406+
},
407+
"required": ["name"],
408+
"additionalProperties": true
409+
}
410+
""";
411+
412+
String contentJson = """
413+
{
414+
"name": "John Doe",
415+
"extraField": "should be allowed"
416+
}
417+
""";
418+
419+
Map<String, Object> schema = toMap(schemaJson);
420+
Map<String, Object> structuredContent = toMap(contentJson);
421+
422+
ValidationResponse response = validator.validate(schema, structuredContent);
423+
424+
assertTrue(response.valid());
425+
assertNull(response.errorMessage());
426+
}
427+
319428
@Test
320429
void testValidateWithAdditionalPropertiesExplicitlyDisallowed() {
321430
String schemaJson = """

0 commit comments

Comments
 (0)