GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — DefaultJsonSchemaValidator caches schemas by hashCode(), so a tool's input can be validated against another tool's schema · Issue #1328 · modelcontextprotocol/java-sdk

DefaultJsonSchemaValidator caches schemas by hashCode(), so a tool's input can be validated against another tool's schema #1328

New issue
Copy link
New issue
Copy link
Open
Open
DefaultJsonSchemaValidator caches schemas by hashCode(), so a tool's input can be validated against another tool's schema#1328
Copy link

Description

@srmadscience
srmadscience
opened on Nov 11, 2026
Issue body actions

Bug description

DefaultJsonSchemaValidator caches each compiled schema under generateCacheKey(schema), which falls back to String.valueOf(schema.hashCode()) when the schema has no $id. Two different schemas with the same hashCode() therefore share one cache entry, and whichever is validated first is used for both. A server validating tool inputs then checks a call to one tool against another tool's input schema:

  • a valid call is rejected (required property 'a' not found), and
  • an invalid call can be accepted.

Which tools are affected depends on the schemas and on the order in which tools are first called, so it can pass tests and fail in production, or the reverse.

Collisions are not rare for tool input schemas. Map.hashCode() sums key.hashCode() ^ value.hashCode() over the entries, and a property name contributes twice (XORed with its sub-schema under properties, and inside the required list), so much of its contribution cancels. Measured with {"type":"object","properties":{N:{"type":"string"}},"required":[N]} over the 702 one- and two-letter names N: only 44 distinct hash codes (17 with "type":"number"; one group holds 190 schemas). Every single-letter name collides with every other.

The code is the same on main and in v2.1.0, in both mcp-json-jackson2 and mcp-json-jackson3 (DefaultJsonSchemaValidator.generateCacheKey).

Environment

MCP Java SDK 2.0.0 (mcp-json-jackson3; also present in 2.1.0 and on main), networknt json-schema-validator 3.0.0, Java 21.0.8. Found in a server exposing two tools with one parameter each, named a and q: after the first had been called, every call to the second was refused.

Steps to reproduce

  1. Create one DefaultJsonSchemaValidator, as a server does.
  2. Validate {"a": "x"} against {"type":"object","properties":{"a":{"type":"string"}},"required":["a"],"additionalProperties":false}.
  3. Validate {"b": "x"} against the same schema with b in place of a.

Expected behavior

Step 3 is valid: each schema is validated as itself. A fresh validator does say valid; the shared one says invalid, with the first schema's errors.

Minimal Complete Reproducible example

import io.modelcontextprotocol.json.schema.jackson3.DefaultJsonSchemaValidator;
import java.util.List;
import java.util.Map;

public class Repro {
    static Map<String, Object> schema(String property) {
        return Map.of("type", "object",
                "properties", Map.of(property, Map.of("type", "string")),
                "required", List.of(property),
                "additionalProperties", false);
    }

    public static void main(String[] args) {
        Map<String, Object> schemaA = schema("a");
        Map<String, Object> schemaB = schema("b");
        System.out.println("schemas equal: " + schemaA.equals(schemaB)
                + ", hashCodes equal: " + (schemaA.hashCode() == schemaB.hashCode()));
        System.out.println("fresh validator, B with {b}: "
                + new DefaultJsonSchemaValidator().validate(schemaB, Map.of("b", "x")).valid());

        DefaultJsonSchemaValidator v = new DefaultJsonSchemaValidator();
        System.out.println("A with {a}: " + v.validate(schemaA, Map.of("a", "x")).valid());
        var r = v.validate(schemaB, Map.of("b", "x"));
        System.out.println("then B with {b} (valid for B): " + r.valid() + "  " + (r.valid() ? "" : r.errorMessage()));
        System.out.println("then B with {a} (invalid for B): " + v.validate(schemaB, Map.of("a", "x")).valid());
    }
}

Output:

schemas equal: false, hashCodes equal: true
fresh validator, B with {b}: true
A with {a}: true
then B with {b} (valid for B): false  Validation failed: JSON schema validation errors: [: required property 'a' not found, : property 'b' is not defined in the schema and the schema does not allow additional properties]
then B with {a} (invalid for B): true

Possible fix

Key the cache by the schema's content rather than its hash code, e.g. its serialized JSON (or String.valueOf(schema) for the nested Map/List values here), so that two keys are equal only when the schemas are. A server can work around it today by passing McpServer...jsonSchemaValidator(...) a DefaultJsonSchemaValidator subclass that overrides the protected generateCacheKey that way.

Activity

  1. Shanmukh-l33t commented on Nov 11, 2026

    @Shanmukh-l33t
    Shanmukh-l33t
    on Nov 11, 2026
    More actions

    Reproduced on main (33e1581) and fixed on a branch — sharing the analysis and approach here.

    Reproduction confirmed. With one shared DefaultJsonSchemaValidator, the two schemas from the report (property a vs b, "additionalProperties": false) are equals() == false but have equal hashCode() (511136268 for both, on any JVM — Map.hashCode() is content-derived). After validating {"a":"x"} against schema A, validating {"b":"x"} against schema B returns:

    Validation failed: JSON schema validation errors: [: required property 'a' not found, : property 'b' is not defined in the schema and the schema does not allow additional properties]
    

    and {"a":"x"} (invalid for B) is accepted. Both failure modes from the report occur: false reject and false accept.

    Root cause. DefaultJsonSchemaValidator.generateCacheKey keys the compiled-schema cache on String.valueOf(schema.hashCode()) for schemas without $id, and on the $id value alone otherwise. Both are collision-prone: Map.hashCode() sums key.hashCode() ^ value.hashCode() terms, so structurally similar schemas collide easily (as the report measured), and two distinct schemas may also share an $id. First validator for a key wins, so every later schema with the same key is checked against the wrong compiled schema.

    Fix approach. Key the cache on the schema's canonical serialization: recursively sort map entries, then take the mapper's JSON string as the key. Two schemas then share a cache entry if and only if their content is equal. The cache's purpose (avoid recompiling with networknt on every validate() call) is unchanged — equal schemas still compile once, regardless of map iteration order. Applied to both mcp-json-jackson2 and mcp-json-jackson3. generateCacheKey keeps its protected signature, so the subclass workaround mentioned in the report still compiles.

    Test evidence (JDK 17, ./mvnw test -pl mcp-json-jackson2,mcp-json-jackson3):

    • New testDistinctSchemasWithCollidingHashCodesDoNotShareCacheEntry asserts the collision first (!schemaA.equals(schemaB) and equal hash codes), then the cross-contamination. On unmodified main: RED — mcp-json-jackson3 Tests run: 47, Failures: 1 and mcp-json-jackson2 Tests run: 47, Failures: 1, both failing exactly at the Expected {b=x} to be valid against schema B assertion with the error above.
    • On the branch: GREEN — mcp-json-jackson3 Tests run: 47, Failures: 0, Errors: 0 and mcp-json-jackson2 Tests run: 47, Failures: 0, Errors: 0 (all pre-existing tests included).
    • Also added testEqualSchemasShareCacheEntry (green on both main and branch) pinning that equal schemas still share one compiled entry (getCacheSize() == 1).

    Tests-first branch (regression tests commit, then fix commit): main...Shanmukh-l33t:fix/json-schema-validator-cache-key — happy to reshape if a maintainer wants a different key strategy (e.g. keep the $id fast path and verify content on hit).

    AI-assistance disclosure: the analysis, tests and fix were prepared with the help of an AI coding agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.