Skip to content

SchemaValidator rejects nested empty JSON objects as arrays #586

Open

Description

@m-vo

Describe the bug
Nested empty objects in tool arguments are rejected as arrays.

To Reproduce

use Mcp\Capability\Discovery\SchemaValidator;

$data = json_decode(
    '{"parameters":{}}',
    true,
    flags: JSON_THROW_ON_ERROR,
);

$schema = [
    'type' => 'object',
    'properties' => [
        'parameters' => ['type' => 'object'],
    ],
];

// Invalid type. Expected `object`, but received `array`.
var_dump((new SchemaValidator())->validateAgainstJsonSchema($data, $schema));

Expected behavior
No validation errors. The original JSON value is an object.

Additional context
json_decode($foo, true) converts both {} and [] to an empty PHP array. The SchemaValidator converts an empty root value to stdClass, but nested empty values remain arrays. The empty arrays should probably be converted recursively according to the schema.

Activity

  1. added
    bugSomething isn't working
    on Oct 10, 2026
  2. jarrettdustinqq commented on Oct 10, 2026

    @jarrettdustinqq

    I confirmed the lossy boundary in the current source. One caution for the proposed recursive conversion: coercing every nested empty PHP array from the nearest schema can fix {}, but it can also turn a real JSON [] into an object when the active schema is a oneOf/anyOf, a local $ref, or an additionalProperties schema that permits both object and array.

    The distinction is discarded before validation on the HTTP paths because the request bodies are decoded with assoc: true; SchemaValidator::convertDataForValidator() then has no way to distinguish the two empty JSON kinds. CallToolHandler repairs only the root empty-arguments case with new stdClass().

    A safe implementation boundary would be either:

    1. preserve the JSON object/array distinction for tool arguments at ingress/deserialization, or
    2. if normalization stays in SchemaValidator, coerce an empty list only when the resolved applicable schema requires object and does not also allow array.

    I would include regressions for: a nested declared object {}; a real nested array []; an array item that is {}; object-valued additionalProperties; a local $ref to an object; and an object-or-array union exercised with both {} and []. The same distinction should be checked on output validation so the direct validator API and tools/call path do not diverge.

    This is a source-level review; I did not run the PHP test suite. AI assistance was used to inspect the current call path and prepare this comment.

  3. codekandis commented on Oct 11, 2026

    @codekandis

    The proposed recursive, schema-based conversion of empty arrays to objects would only work around the symptom rather than fix the underlying problem. Worse, it could introduce incorrect schema validation results.

    The underlying issue is broader than nested empty objects: the SDK unnecessarily converts JSON types into PHP arrays and subsequently attempts to reconstruct the original types for validation.

    Root cause

    json_decode(..., assoc: true) irreversibly loses the distinction between certain JSON objects and arrays.

    This is not limited to empty values:

    Original JSON PHP representation
    {"parameters": {}} ['parameters' => []]
    {"parameters": []} ['parameters' => []]
    {"parameters": {"0": "x"}} ['parameters' => [0 => 'x']]
    {"parameters": ["x"]} ['parameters' => [0 => 'x']]

    Even non-empty JSON objects with sequential numeric property names can become indistinguishable from JSON arrays.

    Incorrect schema validation

    Consider the following schema:

    {
      "type": "object",
      "properties": {
        "parameters": {
          "type": "object"
        }
      },
      "required": ["parameters"]
    }

    The expected validation results are:

    • {"parameters": {}} → Valid
    • {"parameters": []} → Invalid
    • {"parameters": {"0": "x"}} → Valid
    • {"parameters": ["x"]} → Invalid

    With assoc: true, each valid/invalid pair becomes identical before validation.

    A schema-based workaround that reconstructs objects would therefore risk accepting invalid JSON arrays as objects. Without such reconstruction, valid JSON objects can be rejected as arrays.

    Once the type information has been discarded, no schema-dependent conversion can reliably restore it.

    Incorrect root-level validation

    The problem also affects the root value of tool arguments, independently of nested properties.

    Consider this input schema:

    {
      "type": "object",
      "additionalProperties": false
    }

    The expected validation results for the root value of arguments are:

    • {} → Valid
    • [] → Invalid
    • [1, 2, 3] → Invalid

    However, CallToolHandler currently contains this workaround:

    $validationErrors = $this->schemaValidator->validateAgainstJsonSchema(
        [] === $arguments ? new \stdClass() : $arguments,
        $inputSchema
    );

    Consequently, both of these tool argument values can pass validation:

    {"name": "example", "arguments": {}}
    {"name": "example", "arguments": []}

    The second value must be rejected because MCP tool arguments are required to be a JSON object, not a JSON array.

    The workaround actively changes the type before validation, producing a false positive.

    Non-empty arrays remain arrays and are rejected by the object schema, making validation inconsistent depending on whether the array is empty.

    Removing assoc: true alone is insufficient

    Even if JSON decoding preserves the original types, CallToolRequest::fromParams() currently contains another conversion:

    if ($arguments instanceof \stdClass) {
        $arguments = (array) $arguments;
    }

    Furthermore, CallToolRequest::$arguments is explicitly typed as array.

    This means that even correctly decoded JSON objects are converted back into PHP arrays before reaching CallToolHandler.

    In particular, both an empty JSON object and an empty JSON array become [] at this boundary. The handler then converts either value to stdClass for validation.

    Therefore, the existing argument conversion and root-level workaround must also be addressed. Simply changing json_decode() would not resolve the issue.

    Unnecessary conversion logic

    The current implementation involves several conversions and special cases:

    1. MessageFactory decodes JSON using assoc: true.
    2. CallToolRequest enforces PHP arrays for arguments.
    3. SchemaValidator::convertDataForValidator() recursively converts associative arrays into stdClass.
    4. CallToolHandler additionally converts empty root arguments into stdClass.
    5. Tool handlers still receive PHP arrays rather than the original JSON types.

    This introduces unnecessary complexity without guaranteeing correct schema validation or type safety.

    Notably, output validation in CallToolHandler::validateStructuredContent() already uses json_decode(..., false) to preserve the JSON object/array distinction.

    Proposed solution

    Rather than introducing additional schema-dependent conversion workarounds, the SDK should preserve native JSON types throughout the entire processing pipeline:

    1. Decode incoming JSON without assoc: true.
    2. Preserve JSON objects as stdClass and JSON arrays as PHP arrays, including nested values.
    3. Adapt CallToolRequest to retain these types rather than forcing arguments into PHP arrays.
    4. Remove the empty-root-array workaround in CallToolHandler.
    5. Validate the actual JSON types without attempting to reconstruct them from schema definitions.
    6. Pass validated values unchanged to tool handlers, as proposed in Preserve native JSON types when passing arguments to tool handlers #587.

    Consumers preferring associative arrays can explicitly convert the values themselves.

    The public SchemaValidator API may still need a clearly defined policy for PHP arrays supplied directly by application code. However, this should not require the SDK's protocol handling to discard and reconstruct JSON types.

    Conclusion

    This is fundamentally a data integrity, schema validation, and type safety issue rather than merely a problem with nested empty objects.

    Issues #586 and #587 are closely related consequences of the same architectural problem and should ideally be addressed together.

    Preserving native JSON types throughout the SDK would eliminate unnecessary conversions and special cases, simplify the implementation, and ensure that schema validation operates on the actual values supplied by the client.

  4. Chi-teck commented on Oct 16, 2026

    @Chi-teck

    A real-world client that triggers this on every call: Docker Agent (v1.150.0). For OpenAI-compatible providers it rewrites tool schemas so that every property is required and the optional ones become nullable. A model with nothing to pass for an optional type: object parameter must then send null or {}. Docker Agent strips null before the call, but {} goes through and is rejected:

    Invalid parameters for tool 'ctx_database_query': Property '/arguments': Invalid type. Expected `object`, but received `array`.
    

    We see this with mcp/sdk v0.8.1 and the Drupal ctx module's database_query tool, which takes an optional arguments map: every call that sends "arguments": {} is rejected. The model then tries filler arguments, which fail elsewhere, and sometimes gives up on the tool.

    So this isn't limited to hand-crafted requests: any client that normalizes schemas this way hits it with optional object parameters. The client-side workaround is to turn {} into null before the call, which every consumer has to discover on their own. Either fix discussed here would solve it for us, and we'd be glad to test a pull request.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions