You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SchemaValidator rejects nested empty JSON objects as arrays #586
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.
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:
preserve the JSON object/array distinction for tool arguments at ingress/deserialization, or
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.
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.
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.
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 ($argumentsinstanceof \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:
MessageFactory decodes JSON using assoc: true.
CallToolRequest enforces PHP arrays for arguments.
SchemaValidator::convertDataForValidator() recursively converts associative arrays into stdClass.
CallToolHandler additionally converts empty root arguments into stdClass.
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:
Decode incoming JSON without assoc: true.
Preserve JSON objects as stdClass and JSON arrays as PHP arrays, including nested values.
Adapt CallToolRequest to retain these types rather than forcing arguments into PHP arrays.
Remove the empty-root-array workaround in CallToolHandler.
Validate the actual JSON types without attempting to reconstruct them from schema definitions.
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.
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.
Activity
jarrettdustinqq commentedon Oct 10, 2026
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 aoneOf/anyOf, a local$ref, or anadditionalPropertiesschema 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.CallToolHandlerrepairs only the root empty-arguments case withnew stdClass().A safe implementation boundary would be either:
SchemaValidator, coerce an empty list only when the resolved applicable schema requiresobjectand does not also allowarray.I would include regressions for: a nested declared object
{}; a real nested array[]; an array item that is{}; object-valuedadditionalProperties; a local$refto 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 andtools/callpath 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.
Work around empty JSON objects being passed to the MCP layer (see #10436
Work around empty JSON objects being passed to the MCP layer (see #10…
codekandis commentedon Oct 11, 2026
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:
{"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"]}→ InvalidWith
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
argumentsare:{}→ Valid[]→ Invalid[1, 2, 3]→ InvalidHowever,
CallToolHandlercurrently contains this workaround: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: truealone is insufficientEven if JSON decoding preserves the original types,
CallToolRequest::fromParams()currently contains another conversion:Furthermore,
CallToolRequest::$argumentsis explicitly typed asarray.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 tostdClassfor 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:
MessageFactorydecodes JSON usingassoc: true.CallToolRequestenforces PHP arrays for arguments.SchemaValidator::convertDataForValidator()recursively converts associative arrays intostdClass.CallToolHandleradditionally converts empty root arguments intostdClass.This introduces unnecessary complexity without guaranteeing correct schema validation or type safety.
Notably, output validation in
CallToolHandler::validateStructuredContent()already usesjson_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:
assoc: true.stdClassand JSON arrays as PHP arrays, including nested values.CallToolRequestto retain these types rather than forcing arguments into PHP arrays.CallToolHandler.Consumers preferring associative arrays can explicitly convert the values themselves.
The public
SchemaValidatorAPI 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.
Chi-teck commentedon Oct 16, 2026
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: objectparameter must then sendnullor{}. Docker Agent stripsnullbefore the call, but{}goes through and is rejected:We see this with mcp/sdk v0.8.1 and the Drupal
ctxmodule'sdatabase_querytool, which takes an optionalargumentsmap: 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
{}intonullbefore 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.