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:
parent
af65356416
commit
2456a0e433
6 changed files with 2487 additions and 21 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright 2026-2026 the original author or authors.
|
||||
*/
|
||||
package io.modelcontextprotocol.json.schema.jackson3;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.networknt.schema.Schema;
|
||||
import com.networknt.schema.SchemaRegistry;
|
||||
import com.networknt.schema.Error;
|
||||
import com.networknt.schema.dialect.Dialects;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/**
|
||||
* 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 Filip Hrisafov
|
||||
*/
|
||||
public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);
|
||||
|
||||
private final JsonMapper jsonMapper;
|
||||
|
||||
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(JsonMapper.shared());
|
||||
}
|
||||
|
||||
public DefaultJsonSchemaValidator(JsonMapper jsonMapper) {
|
||||
this.jsonMapper = jsonMapper;
|
||||
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.jsonMapper.readTree((String) structuredContent)
|
||||
: this.jsonMapper.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 (JacksonException 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 JacksonException if schema processing fails
|
||||
*/
|
||||
private Schema getOrCreateJsonSchema(Map<String, Object> schema) throws JacksonException {
|
||||
// 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 JacksonException if schema processing fails
|
||||
*/
|
||||
private Schema createJsonSchema(Map<String, Object> schema) throws JacksonException {
|
||||
// Convert schema map directly to JsonNode (more efficient than string
|
||||
// serialization)
|
||||
JsonNode schemaNode = this.jsonMapper.valueToTree(schema);
|
||||
|
||||
// Handle case where ObjectMapper might return null (e.g., in mocked scenarios)
|
||||
if (schemaNode == null) {
|
||||
throw new JacksonException("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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2026 - 2026 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.modelcontextprotocol.json.schema.jackson3;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier
|
||||
|
|
@ -0,0 +1 @@
|
|||
io.modelcontextprotocol.json.schema.jackson3.JacksonJsonSchemaValidatorSupplier
|
||||
Loading…
Reference in a new issue