Add Support for Jackson 3 (#742)

This change adds Jackson 3 support and aligns package naming in the existing Jackson 2 module.

Specific changes:
* Deprecated classes for removal in `io.modelcontextprotocol.json.jackson`/`io.modelcontextprotocol.json.schema.jackson`
* Copied the above to `io.modelcontextprotocol.json.jackson2`/`io.modelcontextprotocol.json.schema.jackson2` in non-deprecated form.
* Added tests for the default McpJsonMapper and JsonSchemaValidator
This commit is contained in:
Filip Hrisafov 2026-01-26 11:43:35 +01:00 • committed by GitHub
commit 2456a0e433
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2487 additions and 21 deletions

View file

@ -80,7 +80,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
<version>${jackson-annotations.version}</version>
</dependency>
<dependency>
@ -100,7 +100,7 @@
<!-- Test dependencies -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json-jackson2</artifactId>
<artifactId>mcp-json-jackson3</artifactId>
<version>0.18.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>

View file

@ -10,6 +10,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -18,7 +19,7 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.exc.InvalidTypeIdException;
import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import net.javacrumbs.jsonunit.core.Option;
@ -58,7 +59,9 @@ public class McpSchemaTests {
void testContentDeserializationWrongType() throws Exception {
assertThatThrownBy(() -> JSON_MAPPER.readValue("""
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class))
{"type":"WRONG","text":"XXX"}""", McpSchema.TextContent.class)).isInstanceOf(IOException.class)
.hasMessage("Failed to read value")
.cause()
.isInstanceOf(InvalidTypeIdException.class)
.hasMessageContaining(
"Could not resolve type id 'WRONG' as a subtype of `io.modelcontextprotocol.spec.McpSchema$TextContent`: known type ids = [audio, image, resource, resource_link, text]");

View file

@ -11,7 +11,7 @@
<artifactId>mcp-json-jackson2</artifactId>
<packaging>jar</packaging>
<name>Java MCP SDK JSON Jackson</name>
<description>Java MCP SDK JSON implementation based on Jackson</description>
<description>Java MCP SDK JSON implementation based on Jackson 2</description>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
<scm>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
@ -42,12 +42,12 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<version>${jackson2.version}</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<version>${json-schema-validator-jackson2.version}</version>
</dependency>
<dependency>

View file

@ -14,7 +14,12 @@ import java.io.IOException;
/**
* Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the
* SDK decoupled from Jackson at the API level.
*
* @deprecated since 18.0.0, use
* {@link io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper} instead. Will be
* removed in 19.0.0
*/
@Deprecated(forRemoval = true, since = "18.0.0")
public final class JacksonMcpJsonMapper implements McpJsonMapper {
private final ObjectMapper objectMapper;

View file

@ -13,7 +13,12 @@ import io.modelcontextprotocol.json.McpJsonMapperSupplier;
* <p>
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
*
* @deprecated since 18.0.0, use
* {@link io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapperSupplier} instead.
* Will be removed in 19.0.0.
*/
@Deprecated(forRemoval = true, since = "18.0.0")
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
/**

View file

@ -0,0 +1,88 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
/**
* Jackson-based implementation of JsonMapper. Wraps a Jackson ObjectMapper but keeps the
* SDK decoupled from Jackson at the API level.
*/
public final class JacksonMcpJsonMapper implements McpJsonMapper {
private final ObjectMapper objectMapper;
/**
* Constructs a new JacksonMcpJsonMapper instance with the given ObjectMapper.
* @param objectMapper the ObjectMapper to be used for JSON serialization and
* deserialization. Must not be null.
* @throws IllegalArgumentException if the provided ObjectMapper is null.
*/
public JacksonMcpJsonMapper(ObjectMapper objectMapper) {
if (objectMapper == null) {
throw new IllegalArgumentException("ObjectMapper must not be null");
}
this.objectMapper = objectMapper;
}
/**
* Returns the underlying Jackson {@link ObjectMapper} used for JSON serialization and
* deserialization.
* @return the ObjectMapper instance
*/
public ObjectMapper getObjectMapper() {
return objectMapper;
}
@Override
public <T> T readValue(String content, Class<T> type) throws IOException {
return objectMapper.readValue(content, type);
}
@Override
public <T> T readValue(byte[] content, Class<T> type) throws IOException {
return objectMapper.readValue(content, type);
}
@Override
public <T> T readValue(String content, TypeRef<T> type) throws IOException {
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
return objectMapper.readValue(content, javaType);
}
@Override
public <T> T readValue(byte[] content, TypeRef<T> type) throws IOException {
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
return objectMapper.readValue(content, javaType);
}
@Override
public <T> T convertValue(Object fromValue, Class<T> type) {
return objectMapper.convertValue(fromValue, type);
}
@Override
public <T> T convertValue(Object fromValue, TypeRef<T> type) {
JavaType javaType = objectMapper.getTypeFactory().constructType(type.getType());
return objectMapper.convertValue(fromValue, javaType);
}
@Override
public String writeValueAsString(Object value) throws IOException {
return objectMapper.writeValueAsString(value);
}
@Override
public byte[] writeValueAsBytes(Object value) throws IOException {
return objectMapper.writeValueAsBytes(value);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson2;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
/**
* A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON
* serialization and deserialization.
* <p>
* This implementation provides a {@link McpJsonMapper} backed by a Jackson
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
*/
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
/**
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
* JSON serialization and deserialization.
* <p>
* The returned {@link McpJsonMapper} is backed by a new instance of
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
* @return a new {@link McpJsonMapper} instance
*/
@Override
public McpJsonMapper get() {
return new JacksonMcpJsonMapper(new com.fasterxml.jackson.databind.ObjectMapper());
}
}

View file

@ -24,7 +24,11 @@ import org.slf4j.LoggerFactory;
* NetworkNT JSON Schema Validator library for validation.
*
* @author Christian Tzolov
* @deprecated since 18.0.0, use
* {@link io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator}
* instead. Will be removed in 19.0.0.
*/
@Deprecated(forRemoval = true, since = "18.0.0")
public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);

View file

@ -13,6 +13,9 @@ import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
*
* @see JsonSchemaValidatorSupplier
* @see JsonSchemaValidator
* @deprecated since 18.0.0, use
* {@link io.modelcontextprotocol.json.schema.jackson2.JacksonJsonSchemaValidatorSupplier}
* instead. Will be removed in 19.0.0.
*/
public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier {

View file

@ -0,0 +1,163 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.json.schema.jackson2;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.Error;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.dialect.Dialects;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
/**
* Default implementation of the {@link JsonSchemaValidator} interface. This class
* provides methods to validate structured content against a JSON schema. It uses the
* NetworkNT JSON Schema Validator library for validation.
*
* @author Christian Tzolov
*/
public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);
private final ObjectMapper objectMapper;
private final SchemaRegistry schemaFactory;
// TODO: Implement a strategy to purge the cache (TTL, size limit, etc.)
private final ConcurrentHashMap<String, Schema> schemaCache;
public DefaultJsonSchemaValidator() {
this(new ObjectMapper());
}
public DefaultJsonSchemaValidator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.schemaFactory = SchemaRegistry.withDialect(Dialects.getDraft202012());
this.schemaCache = new ConcurrentHashMap<>();
}
@Override
public ValidationResponse validate(Map<String, Object> schema, Object structuredContent) {
if (schema == null) {
throw new IllegalArgumentException("Schema must not be null");
}
if (structuredContent == null) {
throw new IllegalArgumentException("Structured content must not be null");
}
try {
JsonNode jsonStructuredOutput = (structuredContent instanceof String)
? this.objectMapper.readTree((String) structuredContent)
: this.objectMapper.valueToTree(structuredContent);
List<Error> validationResult = this.getOrCreateJsonSchema(schema).validate(jsonStructuredOutput);
// Check if validation passed
if (!validationResult.isEmpty()) {
return ValidationResponse
.asInvalid("Validation failed: structuredContent does not match tool outputSchema. "
+ "Validation errors: " + validationResult);
}
return ValidationResponse.asValid(jsonStructuredOutput.toString());
}
catch (JsonProcessingException e) {
logger.error("Failed to validate CallToolResult: Error parsing schema: {}", e);
return ValidationResponse.asInvalid("Error parsing tool JSON Schema: " + e.getMessage());
}
catch (Exception e) {
logger.error("Failed to validate CallToolResult: Unexpected error: {}", e);
return ValidationResponse.asInvalid("Unexpected validation error: " + e.getMessage());
}
}
/**
* Gets a cached Schema or creates and caches a new one.
* @param schema the schema map to convert
* @return the compiled Schema
* @throws JsonProcessingException if schema processing fails
*/
private Schema getOrCreateJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
// Generate cache key based on schema content
String cacheKey = this.generateCacheKey(schema);
// Try to get from cache first
Schema cachedSchema = this.schemaCache.get(cacheKey);
if (cachedSchema != null) {
return cachedSchema;
}
// Create new schema if not in cache
Schema newSchema = this.createJsonSchema(schema);
// Cache the schema
Schema existingSchema = this.schemaCache.putIfAbsent(cacheKey, newSchema);
return existingSchema != null ? existingSchema : newSchema;
}
/**
* Creates a new Schema from the given schema map.
* @param schema the schema map
* @return the compiled Schema
* @throws JsonProcessingException if schema processing fails
*/
private Schema createJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
// Convert schema map directly to JsonNode (more efficient than string
// serialization)
JsonNode schemaNode = this.objectMapper.valueToTree(schema);
// Handle case where ObjectMapper might return null (e.g., in mocked scenarios)
if (schemaNode == null) {
throw new JsonProcessingException("Failed to convert schema to JsonNode") {
};
}
return this.schemaFactory.getSchema(schemaNode);
}
/**
* Generates a cache key for the given schema map.
* @param schema the schema map
* @return a cache key string
*/
protected String generateCacheKey(Map<String, Object> schema) {
if (schema.containsKey("$id")) {
// Use the (optional) "$id" field as the cache key if present
return "" + schema.get("$id");
}
// Fall back to schema's hash code as a simple cache key
// For more sophisticated caching, could use content-based hashing
return String.valueOf(schema.hashCode());
}
/**
* Clears the schema cache. Useful for testing or memory management.
*/
public void clearCache() {
this.schemaCache.clear();
}
/**
* Returns the current size of the schema cache.
* @return the number of cached schemas
*/
public int getCacheSize() {
return this.schemaCache.size();
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.json.schema.jackson2;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.json.schema.JsonSchemaValidatorSupplier;
/**
* A concrete implementation of {@link JsonSchemaValidatorSupplier} that provides a
* {@link JsonSchemaValidator} instance based on the Jackson library.
*
* @see JsonSchemaValidatorSupplier
* @see JsonSchemaValidator
*/
public class JacksonJsonSchemaValidatorSupplier implements JsonSchemaValidatorSupplier {
/**
* Returns a new instance of {@link JsonSchemaValidator} that uses the Jackson library
* for JSON schema validation.
* @return A {@link JsonSchemaValidator} instance.
*/
@Override
public JsonSchemaValidator get() {
return new DefaultJsonSchemaValidator();
}
}

View file

@ -1 +1 @@
io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier
io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapperSupplier

View file

@ -1 +1 @@
io.modelcontextprotocol.json.schema.jackson.JacksonJsonSchemaValidatorSupplier
io.modelcontextprotocol.json.schema.jackson2.JacksonJsonSchemaValidatorSupplier

View file

@ -0,0 +1,20 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.json;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
class McpJsonMapperTest {
@Test
void shouldUseJackson2Mapper() {
assertThat(McpJsonMapper.getDefault()).isInstanceOf(JacksonMcpJsonMapper.class);
}
}

View file

@ -0,0 +1,809 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
/**
* Tests for {@link DefaultJsonSchemaValidator}.
*
* @author Christian Tzolov
*/
@Deprecated(forRemoval = true)
class DefaultJsonSchemaValidatorTests {
private DefaultJsonSchemaValidator validator;
private ObjectMapper objectMapper;
@Mock
private ObjectMapper mockObjectMapper;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
validator = new DefaultJsonSchemaValidator();
objectMapper = new ObjectMapper();
}
/**
* Utility method to convert JSON string to Map<String, Object>
*/
private Map<String, Object> toMap(String json) {
try {
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
private List<Map<String, Object>> toListMap(String json) {
try {
return objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
@Test
void testDefaultConstructor() {
DefaultJsonSchemaValidator defaultValidator = new DefaultJsonSchemaValidator();
String schemaJson = """
{
"type": "object",
"properties": {
"test": {"type": "string"}
}
}
""";
String contentJson = """
{
"test": "value"
}
""";
ValidationResponse response = defaultValidator.validate(toMap(schemaJson), toMap(contentJson));
assertTrue(response.valid());
}
@Test
void testConstructorWithObjectMapper() {
ObjectMapper customMapper = new ObjectMapper();
DefaultJsonSchemaValidator customValidator = new DefaultJsonSchemaValidator(customMapper);
String schemaJson = """
{
"type": "object",
"properties": {
"test": {"type": "string"}
}
}
""";
String contentJson = """
{
"test": "value"
}
""";
ValidationResponse response = customValidator.validate(toMap(schemaJson), toMap(contentJson));
assertTrue(response.valid());
}
@Test
void testValidateWithValidStringSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe",
"age": 30
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
assertNotNull(response.jsonStructuredOutput());
}
@Test
void testValidateWithValidNumberSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"price": {"type": "number", "minimum": 0},
"quantity": {"type": "integer", "minimum": 1}
},
"required": ["price", "quantity"]
}
""";
String contentJson = """
{
"price": 19.99,
"quantity": 5
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithValidArraySchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["items"]
}
""";
String contentJson = """
{
"items": ["apple", "banana", "cherry"]
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithValidArraySchemaTopLevelArray() {
String schemaJson = """
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"city" : {
"type" : "string"
},
"summary" : {
"type" : "string"
},
"temperatureC" : {
"type" : "number",
"format" : "float"
}
},
"required" : [ "city", "summary", "temperatureC" ]
},
"additionalProperties" : false
}
""";
String contentJson = """
[
{
"city": "London",
"summary": "Generally mild with frequent rainfall. Winters are cool and damp, summers are warm but rarely hot. Cloudy conditions are common throughout the year.",
"temperatureC": 11.3
},
{
"city": "New York",
"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.",
"temperatureC": 12.8
},
{
"city": "San Francisco",
"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.",
"temperatureC": 14.6
},
{
"city": "Tokyo",
"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.",
"temperatureC": 15.4
}
]
""";
Map<String, Object> schema = toMap(schemaJson);
// Validate as JSON string
ValidationResponse response = validator.validate(schema, contentJson);
assertTrue(response.valid());
assertNull(response.errorMessage());
List<Map<String, Object>> structuredContent = toListMap(contentJson);
// Validate as List<Map<String, Object>>
response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithInvalidTypeSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe",
"age": "thirty"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
assertTrue(response.errorMessage().contains("structuredContent does not match tool outputSchema"));
}
@Test
void testValidateWithMissingRequiredField() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithAdditionalPropertiesNotAllowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should not be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithAdditionalPropertiesExplicitlyAllowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": true
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithDefaultAdditionalProperties() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": true
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithAdditionalPropertiesExplicitlyDisallowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should not be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithEmptySchema() {
String schemaJson = """
{
"additionalProperties": true
}
""";
String contentJson = """
{
"anything": "goes"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithEmptyContent() {
String schemaJson = """
{
"type": "object",
"properties": {}
}
""";
String contentJson = """
{}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithNestedObjectSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name", "address"]
}
},
"required": ["person"]
}
""";
String contentJson = """
{
"person": {
"name": "John Doe",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithInvalidNestedObjectSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name", "address"]
}
},
"required": ["person"]
}
""";
String contentJson = """
{
"person": {
"name": "John Doe",
"address": {
"street": "123 Main St"
}
}
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithJsonProcessingException() throws Exception {
DefaultJsonSchemaValidator validatorWithMockMapper = new DefaultJsonSchemaValidator(mockObjectMapper);
Map<String, Object> schema = Map.of("type", "object");
Map<String, Object> structuredContent = Map.of("key", "value");
// This will trigger our null check and throw JsonProcessingException
when(mockObjectMapper.valueToTree(any())).thenReturn(null);
ValidationResponse response = validatorWithMockMapper.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Error parsing tool JSON Schema"));
assertTrue(response.errorMessage().contains("Failed to convert schema to JsonNode"));
}
@ParameterizedTest
@MethodSource("provideValidSchemaAndContentPairs")
void testValidateWithVariousValidInputs(Map<String, Object> schema, Map<String, Object> content) {
ValidationResponse response = validator.validate(schema, content);
assertTrue(response.valid(), "Expected validation to pass for schema: " + schema + " and content: " + content);
assertNull(response.errorMessage());
}
@ParameterizedTest
@MethodSource("provideInvalidSchemaAndContentPairs")
void testValidateWithVariousInvalidInputs(Map<String, Object> schema, Map<String, Object> content) {
ValidationResponse response = validator.validate(schema, content);
assertFalse(response.valid(), "Expected validation to fail for schema: " + schema + " and content: " + content);
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
private static Map<String, Object> staticToMap(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
private static Stream<Arguments> provideValidSchemaAndContentPairs() {
return Stream.of(
// Boolean schema
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"flag": {"type": "boolean"}
}
}
"""), staticToMap("""
{
"flag": true
}
""")),
// String with additional properties allowed
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": true
}
"""), staticToMap("""
{
"name": "test",
"extra": "allowed"
}
""")),
// Array with specific items
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"}
}
}
}
"""), staticToMap("""
{
"numbers": [1.0, 2.5, 3.14]
}
""")),
// Enum validation
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
"""), staticToMap("""
{
"status": "active"
}
""")));
}
private static Stream<Arguments> provideInvalidSchemaAndContentPairs() {
return Stream.of(
// Wrong boolean type
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"flag": {"type": "boolean"}
}
}
"""), staticToMap("""
{
"flag": "true"
}
""")),
// Array with wrong item types
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"}
}
}
}
"""), staticToMap("""
{
"numbers": ["one", "two", "three"]
}
""")),
// Invalid enum value
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
"""), staticToMap("""
{
"status": "unknown"
}
""")),
// Minimum constraint violation
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"age": {"type": "integer", "minimum": 0}
}
}
"""), staticToMap("""
{
"age": -5
}
""")));
}
@Test
void testValidationResponseToValid() {
String jsonOutput = "{\"test\":\"value\"}";
ValidationResponse response = ValidationResponse.asValid(jsonOutput);
assertTrue(response.valid());
assertNull(response.errorMessage());
assertEquals(jsonOutput, response.jsonStructuredOutput());
}
@Test
void testValidationResponseToInvalid() {
String errorMessage = "Test error message";
ValidationResponse response = ValidationResponse.asInvalid(errorMessage);
assertFalse(response.valid());
assertEquals(errorMessage, response.errorMessage());
assertNull(response.jsonStructuredOutput());
}
@Test
void testValidationResponseRecord() {
ValidationResponse response1 = new ValidationResponse(true, null, "{\"valid\":true}");
ValidationResponse response2 = new ValidationResponse(false, "Error", null);
assertTrue(response1.valid());
assertNull(response1.errorMessage());
assertEquals("{\"valid\":true}", response1.jsonStructuredOutput());
assertFalse(response2.valid());
assertEquals("Error", response2.errorMessage());
assertNull(response2.jsonStructuredOutput());
// Test equality
ValidationResponse response3 = new ValidationResponse(true, null, "{\"valid\":true}");
assertEquals(response1, response3);
assertNotEquals(response1, response2);
}
}

View file

@ -0,0 +1,808 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
/**
* Tests for {@link DefaultJsonSchemaValidator}.
*
* @author Christian Tzolov
*/
class DefaultJsonSchemaValidatorTests {
private DefaultJsonSchemaValidator validator;
private ObjectMapper objectMapper;
@Mock
private ObjectMapper mockObjectMapper;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
validator = new DefaultJsonSchemaValidator();
objectMapper = new ObjectMapper();
}
/**
* Utility method to convert JSON string to Map<String, Object>
*/
private Map<String, Object> toMap(String json) {
try {
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
private List<Map<String, Object>> toListMap(String json) {
try {
return objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
@Test
void testDefaultConstructor() {
DefaultJsonSchemaValidator defaultValidator = new DefaultJsonSchemaValidator();
String schemaJson = """
{
"type": "object",
"properties": {
"test": {"type": "string"}
}
}
""";
String contentJson = """
{
"test": "value"
}
""";
ValidationResponse response = defaultValidator.validate(toMap(schemaJson), toMap(contentJson));
assertTrue(response.valid());
}
@Test
void testConstructorWithObjectMapper() {
ObjectMapper customMapper = new ObjectMapper();
DefaultJsonSchemaValidator customValidator = new DefaultJsonSchemaValidator(customMapper);
String schemaJson = """
{
"type": "object",
"properties": {
"test": {"type": "string"}
}
}
""";
String contentJson = """
{
"test": "value"
}
""";
ValidationResponse response = customValidator.validate(toMap(schemaJson), toMap(contentJson));
assertTrue(response.valid());
}
@Test
void testValidateWithValidStringSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe",
"age": 30
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
assertNotNull(response.jsonStructuredOutput());
}
@Test
void testValidateWithValidNumberSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"price": {"type": "number", "minimum": 0},
"quantity": {"type": "integer", "minimum": 1}
},
"required": ["price", "quantity"]
}
""";
String contentJson = """
{
"price": 19.99,
"quantity": 5
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithValidArraySchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["items"]
}
""";
String contentJson = """
{
"items": ["apple", "banana", "cherry"]
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithValidArraySchemaTopLevelArray() {
String schemaJson = """
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"city" : {
"type" : "string"
},
"summary" : {
"type" : "string"
},
"temperatureC" : {
"type" : "number",
"format" : "float"
}
},
"required" : [ "city", "summary", "temperatureC" ]
},
"additionalProperties" : false
}
""";
String contentJson = """
[
{
"city": "London",
"summary": "Generally mild with frequent rainfall. Winters are cool and damp, summers are warm but rarely hot. Cloudy conditions are common throughout the year.",
"temperatureC": 11.3
},
{
"city": "New York",
"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.",
"temperatureC": 12.8
},
{
"city": "San Francisco",
"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.",
"temperatureC": 14.6
},
{
"city": "Tokyo",
"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.",
"temperatureC": 15.4
}
]
""";
Map<String, Object> schema = toMap(schemaJson);
// Validate as JSON string
ValidationResponse response = validator.validate(schema, contentJson);
assertTrue(response.valid());
assertNull(response.errorMessage());
List<Map<String, Object>> structuredContent = toListMap(contentJson);
// Validate as List<Map<String, Object>>
response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithInvalidTypeSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe",
"age": "thirty"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
assertTrue(response.errorMessage().contains("structuredContent does not match tool outputSchema"));
}
@Test
void testValidateWithMissingRequiredField() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
""";
String contentJson = """
{
"name": "John Doe"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithAdditionalPropertiesNotAllowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should not be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithAdditionalPropertiesExplicitlyAllowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": true
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithDefaultAdditionalProperties() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": true
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithAdditionalPropertiesExplicitlyDisallowed() {
String schemaJson = """
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"],
"additionalProperties": false
}
""";
String contentJson = """
{
"name": "John Doe",
"extraField": "should not be allowed"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithEmptySchema() {
String schemaJson = """
{
"additionalProperties": true
}
""";
String contentJson = """
{
"anything": "goes"
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithEmptyContent() {
String schemaJson = """
{
"type": "object",
"properties": {}
}
""";
String contentJson = """
{}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithNestedObjectSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name", "address"]
}
},
"required": ["person"]
}
""";
String contentJson = """
{
"person": {
"name": "John Doe",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertTrue(response.valid());
assertNull(response.errorMessage());
}
@Test
void testValidateWithInvalidNestedObjectSchema() {
String schemaJson = """
{
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name", "address"]
}
},
"required": ["person"]
}
""";
String contentJson = """
{
"person": {
"name": "John Doe",
"address": {
"street": "123 Main St"
}
}
}
""";
Map<String, Object> schema = toMap(schemaJson);
Map<String, Object> structuredContent = toMap(contentJson);
ValidationResponse response = validator.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
@Test
void testValidateWithJsonProcessingException() throws Exception {
DefaultJsonSchemaValidator validatorWithMockMapper = new DefaultJsonSchemaValidator(mockObjectMapper);
Map<String, Object> schema = Map.of("type", "object");
Map<String, Object> structuredContent = Map.of("key", "value");
// This will trigger our null check and throw JsonProcessingException
when(mockObjectMapper.valueToTree(any())).thenReturn(null);
ValidationResponse response = validatorWithMockMapper.validate(schema, structuredContent);
assertFalse(response.valid());
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Error parsing tool JSON Schema"));
assertTrue(response.errorMessage().contains("Failed to convert schema to JsonNode"));
}
@ParameterizedTest
@MethodSource("provideValidSchemaAndContentPairs")
void testValidateWithVariousValidInputs(Map<String, Object> schema, Map<String, Object> content) {
ValidationResponse response = validator.validate(schema, content);
assertTrue(response.valid(), "Expected validation to pass for schema: " + schema + " and content: " + content);
assertNull(response.errorMessage());
}
@ParameterizedTest
@MethodSource("provideInvalidSchemaAndContentPairs")
void testValidateWithVariousInvalidInputs(Map<String, Object> schema, Map<String, Object> content) {
ValidationResponse response = validator.validate(schema, content);
assertFalse(response.valid(), "Expected validation to fail for schema: " + schema + " and content: " + content);
assertNotNull(response.errorMessage());
assertTrue(response.errorMessage().contains("Validation failed"));
}
private static Map<String, Object> staticToMap(String json) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
}
catch (Exception e) {
throw new RuntimeException("Failed to parse JSON: " + json, e);
}
}
private static Stream<Arguments> provideValidSchemaAndContentPairs() {
return Stream.of(
// Boolean schema
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"flag": {"type": "boolean"}
}
}
"""), staticToMap("""
{
"flag": true
}
""")),
// String with additional properties allowed
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": true
}
"""), staticToMap("""
{
"name": "test",
"extra": "allowed"
}
""")),
// Array with specific items
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"}
}
}
}
"""), staticToMap("""
{
"numbers": [1.0, 2.5, 3.14]
}
""")),
// Enum validation
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
"""), staticToMap("""
{
"status": "active"
}
""")));
}
private static Stream<Arguments> provideInvalidSchemaAndContentPairs() {
return Stream.of(
// Wrong boolean type
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"flag": {"type": "boolean"}
}
}
"""), staticToMap("""
{
"flag": "true"
}
""")),
// Array with wrong item types
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"numbers": {
"type": "array",
"items": {"type": "number"}
}
}
}
"""), staticToMap("""
{
"numbers": ["one", "two", "three"]
}
""")),
// Invalid enum value
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}
"""), staticToMap("""
{
"status": "unknown"
}
""")),
// Minimum constraint violation
Arguments.of(staticToMap("""
{
"type": "object",
"properties": {
"age": {"type": "integer", "minimum": 0}
}
}
"""), staticToMap("""
{
"age": -5
}
""")));
}
@Test
void testValidationResponseToValid() {
String jsonOutput = "{\"test\":\"value\"}";
ValidationResponse response = ValidationResponse.asValid(jsonOutput);
assertTrue(response.valid());
assertNull(response.errorMessage());
assertEquals(jsonOutput, response.jsonStructuredOutput());
}
@Test
void testValidationResponseToInvalid() {
String errorMessage = "Test error message";
ValidationResponse response = ValidationResponse.asInvalid(errorMessage);
assertFalse(response.valid());
assertEquals(errorMessage, response.errorMessage());
assertNull(response.jsonStructuredOutput());
}
@Test
void testValidationResponseRecord() {
ValidationResponse response1 = new ValidationResponse(true, null, "{\"valid\":true}");
ValidationResponse response2 = new ValidationResponse(false, "Error", null);
assertTrue(response1.valid());
assertNull(response1.errorMessage());
assertEquals("{\"valid\":true}", response1.jsonStructuredOutput());
assertFalse(response2.valid());
assertEquals("Error", response2.errorMessage());
assertNull(response2.jsonStructuredOutput());
// Test equality
ValidationResponse response3 = new ValidationResponse(true, null, "{\"valid\":true}");
assertEquals(response1, response3);
assertNotEquals(response1, response2);
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2026-2026 the original author or authors.
*/
package io.modelcontextprotocol.json.schema;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
class JsonSchemaValidatorTest {
@Test
void shouldUseJackson2Mapper() {
assertThat(JsonSchemaValidator.getDefault()).isInstanceOf(DefaultJsonSchemaValidator.class);
}
}

79
mcp-json-jackson3/pom.xml Normal file
View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId>
<version>0.18.0-SNAPSHOT</version>
</parent>
<artifactId>mcp-json-jackson3</artifactId>
<packaging>jar</packaging>
<name>Java MCP SDK JSON Jackson</name>
<description>Java MCP SDK JSON implementation based on Jackson 3</description>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
<scm>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
<developerConnection>git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
</scm>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-json</artifactId>
<version>0.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson3.version}</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator-jackson3.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assert4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,119 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson3;
import java.io.IOException;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.json.JsonMapper;
/**
* Jackson-based implementation of JsonMapper. Wraps a Jackson JsonMapper but keeps the
* SDK decoupled from Jackson at the API level.
*/
public final class JacksonMcpJsonMapper implements McpJsonMapper {
private final JsonMapper jsonMapper;
/**
* Constructs a new JacksonMcpJsonMapper instance with the given JsonMapper.
* @param jsonMapper the JsonMapper to be used for JSON serialization and
* deserialization. Must not be null.
* @throws IllegalArgumentException if the provided JsonMapper is null.
*/
public JacksonMcpJsonMapper(JsonMapper jsonMapper) {
if (jsonMapper == null) {
throw new IllegalArgumentException("JsonMapper must not be null");
}
this.jsonMapper = jsonMapper;
}
/**
* Returns the underlying Jackson {@link JsonMapper} used for JSON serialization and
* deserialization.
* @return the JsonMapper instance
*/
public JsonMapper getJsonMapper() {
return jsonMapper;
}
@Override
public <T> T readValue(String content, Class<T> type) throws IOException {
try {
return jsonMapper.readValue(content, type);
}
catch (JacksonException ex) {
throw new IOException("Failed to read value", ex);
}
}
@Override
public <T> T readValue(byte[] content, Class<T> type) throws IOException {
try {
return jsonMapper.readValue(content, type);
}
catch (JacksonException ex) {
throw new IOException("Failed to read value", ex);
}
}
@Override
public <T> T readValue(String content, TypeRef<T> type) throws IOException {
JavaType javaType = jsonMapper.getTypeFactory().constructType(type.getType());
try {
return jsonMapper.readValue(content, javaType);
}
catch (JacksonException ex) {
throw new IOException("Failed to read value", ex);
}
}
@Override
public <T> T readValue(byte[] content, TypeRef<T> type) throws IOException {
JavaType javaType = jsonMapper.getTypeFactory().constructType(type.getType());
try {
return jsonMapper.readValue(content, javaType);
}
catch (JacksonException ex) {
throw new IOException("Failed to read value", ex);
}
}
@Override
public <T> T convertValue(Object fromValue, Class<T> type) {
return jsonMapper.convertValue(fromValue, type);
}
@Override
public <T> T convertValue(Object fromValue, TypeRef<T> type) {
JavaType javaType = jsonMapper.getTypeFactory().constructType(type.getType());
return jsonMapper.convertValue(fromValue, javaType);
}
@Override
public String writeValueAsString(Object value) throws IOException {
try {
return jsonMapper.writeValueAsString(value);
}
catch (JacksonException ex) {
throw new IOException("Failed to write value as string", ex);
}
}
@Override
public byte[] writeValueAsBytes(Object value) throws IOException {
try {
return jsonMapper.writeValueAsBytes(value);
}
catch (JacksonException ex) {
throw new IOException("Failed to write value as bytes", ex);
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2026 - 2026 the original author or authors.
*/
package io.modelcontextprotocol.json.jackson3;
import tools.jackson.databind.json.JsonMapper;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.McpJsonMapperSupplier;
/**
* A supplier of {@link McpJsonMapper} instances that uses the Jackson library for JSON
* serialization and deserialization.
* <p>
* This implementation provides a {@link McpJsonMapper} backed by
* {@link JsonMapper#shared() JsonMapper shared instance}.
*/
public class JacksonMcpJsonMapperSupplier implements McpJsonMapperSupplier {
/**
* Returns a new instance of {@link McpJsonMapper} that uses the Jackson library for
* JSON serialization and deserialization.
* <p>
* The returned {@link McpJsonMapper} is backed by {@link JsonMapper#shared()
* JsonMapper shared instance}.
* @return a new {@link McpJsonMapper} instance
*/
@Override
public McpJsonMapper get() {
return new JacksonMcpJsonMapper(JsonMapper.shared());
}
}

Some files were not shown because too many files have changed in this diff Show more