feat(schema): add support for JSON Schema $defs and definitions (#146)

Added support for $defs and definitions properties in JsonSchema record to handle JSON Schema references properly. Added tests to verify both formats work correctly.

The JsonSchema test approach uses serialization/deserialization round-trip validation instead of property-by-property assertions. This makes tests more maintainable and less likely to break when new properties are added.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Richie Caputo 2025-04-23 10:47:48 -04:00 • committed by GitHub
commit f70b98b4b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 132 additions and 1 deletions

View file

@ -703,7 +703,9 @@ public final class McpSchema {
@JsonProperty("type") String type,
@JsonProperty("properties") Map<String, Object> properties,
@JsonProperty("required") List<String> required,
@JsonProperty("additionalProperties") Boolean additionalProperties) {
@JsonProperty("additionalProperties") Boolean additionalProperties,
@JsonProperty("$defs") Map<String, Object> defs,
@JsonProperty("definitions") Map<String, Object> definitions) {
} // @formatter:on
/**

View file

@ -9,6 +9,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
@ -449,6 +450,92 @@ public class McpSchemaTests {
// Tool Tests
@Test
void testJsonSchema() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"$ref": "#/$defs/Address"
}
},
"required": ["name"],
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
}
}
""";
// Deserialize the original string to a JsonSchema object
McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
// Serialize the object back to a string
String serialized = mapper.writeValueAsString(schema);
// Deserialize again
McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
// Serialize one more time and compare with the first serialization
String serializedAgain = mapper.writeValueAsString(deserialized);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
}
@Test
void testJsonSchemaWithDefinitions() throws Exception {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"$ref": "#/definitions/Address"
}
},
"required": ["name"],
"definitions": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
}
}
""";
// Deserialize the original string to a JsonSchema object
McpSchema.JsonSchema schema = mapper.readValue(schemaJson, McpSchema.JsonSchema.class);
// Serialize the object back to a string
String serialized = mapper.writeValueAsString(schema);
// Deserialize again
McpSchema.JsonSchema deserialized = mapper.readValue(serialized, McpSchema.JsonSchema.class);
// Serialize one more time and compare with the first serialization
String serializedAgain = mapper.writeValueAsString(deserialized);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
}
@Test
void testTool() throws Exception {
String schemaJson = """
@ -477,6 +564,48 @@ public class McpSchemaTests {
{"name":"test-tool","description":"A test tool","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"number"}},"required":["name"]}}"""));
}
@Test
void testToolWithComplexSchema() throws Exception {
String complexSchemaJson = """
{
"type": "object",
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"properties": {
"name": {"type": "string"},
"shippingAddress": {"$ref": "#/$defs/Address"}
},
"required": ["name", "shippingAddress"]
}
""";
McpSchema.Tool tool = new McpSchema.Tool("addressTool", "Handles addresses", complexSchemaJson);
// Serialize the tool to a string
String serialized = mapper.writeValueAsString(tool);
// Deserialize back to a Tool object
McpSchema.Tool deserializedTool = mapper.readValue(serialized, McpSchema.Tool.class);
// Serialize again and compare with first serialization
String serializedAgain = mapper.writeValueAsString(deserializedTool);
// The two serialized strings should be the same
assertThatJson(serializedAgain).when(Option.IGNORING_ARRAY_ORDER).isEqualTo(json(serialized));
// Just verify the basic structure was preserved
assertThat(deserializedTool.inputSchema().defs()).isNotNull();
assertThat(deserializedTool.inputSchema().defs()).containsKey("Address");
}
@Test
void testCallToolRequest() throws Exception {
Map<String, Object> arguments = new HashMap<>();