From 8b77f8b242b19d9fdb8cc9eac5f693686d7b9ff1 Mon Sep 17 00:00:00 2001 From: Ahmet Soormally Date: Fri, 5 Jun 2026 22:20:15 +0100 Subject: [PATCH] fix(jsonschema): root variables object is always a non-nullable "object" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1518 was correct but incomplete. Switching nullability from the OpenAPI 3.0 "nullable" keyword to the JSON Schema 2020-12 form was right, but it exposed a latent defect: GetSchema() only forced the root variables object non-nullable when the operation had a required variable. Operations with all-optional variables kept a nullable root, which previously serialized to {"type":"object","nullable":true} and was harmless because validators ignore the unknown "nullable" keyword. After #1518 it serializes to {"type":["object","null"]}, which strict consumers reject — the MCP go-sdk's AddTool requires the input schema type to be exactly "object" and panics otherwise: panic: AddTool "list_employees": input schema must have type "object" (got [object null]) This broke the cosmo router engine bump 2.4.2 -> 2.4.3 (pkg/mcpserver, protocol and security suites). The root variables object is always a concrete object — the container is present or omitted, never the JSON literal null — so set Nullable=false unconditionally in GetSchema(). This is consistent with nested input-object variables, which are already forced non-nullable. Only individual optional fields remain nullable. Update the affected golden tests and rename the root_schema_nullable_based_on_required_arguments subtest accordingly. Fixes ENG-9682 --- v2/pkg/engine/jsonschema/variables_schema.go | 10 +++--- .../jsonschema/variables_schema_test.go | 32 ++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/v2/pkg/engine/jsonschema/variables_schema.go b/v2/pkg/engine/jsonschema/variables_schema.go index ebd8ba1abe..827f3749a8 100644 --- a/v2/pkg/engine/jsonschema/variables_schema.go +++ b/v2/pkg/engine/jsonschema/variables_schema.go @@ -156,10 +156,12 @@ func (v *VariablesSchemaBuilder) EnterVariableDefinition(ref int) { // GetSchema returns the built schema func (v *VariablesSchemaBuilder) GetSchema() *JsonSchema { - // If we have required fields, the root schema cannot be nullable - if len(v.schema.Required) > 0 { - v.schema.Nullable = false - } + // The root variables object is always a concrete object and must never be + // nullable: the variables container is either present or omitted, never the + // JSON literal null. Emitting a nullable root (type ["object","null"] under + // JSON Schema 2020-12) breaks strict consumers such as the MCP SDK, which + // require the input schema's type to be exactly "object". + v.schema.Nullable = false // Attach definitions for any recursive input types referenced via "$ref" if len(v.defs) > 0 { v.schema.Defs = v.defs diff --git a/v2/pkg/engine/jsonschema/variables_schema_test.go b/v2/pkg/engine/jsonschema/variables_schema_test.go index 8fd152be04..467fe4d6d9 100644 --- a/v2/pkg/engine/jsonschema/variables_schema_test.go +++ b/v2/pkg/engine/jsonschema/variables_schema_test.go @@ -117,10 +117,7 @@ func TestBuildJsonSchema(t *testing.T) { "type": "object" } }, - "type": [ - "object", - "null" - ] + "type": "object" }` // Compare actual JSON with expected JSON @@ -1362,17 +1359,14 @@ func TestBuildJsonSchema(t *testing.T) { "type": "object" } }, - "type": [ - "object", - "null" - ] + "type": "object" }` // Compare actual JSON with expected JSON assert.JSONEq(t, expectedJSON, string(data), "JSON schema does not match expected structure") }) - t.Run("root schema nullable based on required arguments", func(t *testing.T) { + t.Run("root schema is always a non-nullable object", func(t *testing.T) { // Define schema with required and optional arguments schemaSDL := scalarDefinitions + ` schema { @@ -1452,7 +1446,10 @@ func TestBuildJsonSchema(t *testing.T) { data2, err := json.MarshalIndent(schema2, "", " ") require.NoError(t, err) - // Define expected JSON schema for optional argument case + // Define expected JSON schema for optional argument case. + // Even when every variable is optional, the root variables object stays a + // non-nullable "object": the container is omitted or present, never the + // JSON literal null. Only the individual optional fields are nullable. expectedJSON2 := `{ "additionalProperties": false, "properties": { @@ -1463,10 +1460,7 @@ func TestBuildJsonSchema(t *testing.T) { ] } }, - "type": [ - "object", - "null" - ] + "type": "object" }` // Compare actual JSON with expected JSON @@ -1553,10 +1547,7 @@ func TestBuildJsonSchema(t *testing.T) { "type": "object" } }, - "type": [ - "object", - "null" - ] + "type": "object" }` // Compare actual JSON with expected JSON @@ -1624,10 +1615,7 @@ func TestBuildJsonSchema(t *testing.T) { "description": "ISO-8601 date time format" } }, - "type": [ - "object", - "null" - ] + "type": "object" }` // Compare actual JSON with expected JSON