diff --git a/v2/pkg/engine/jsonschema/nullable_2020_12_test.go b/v2/pkg/engine/jsonschema/nullable_2020_12_test.go new file mode 100644 index 0000000000..5973023a79 --- /dev/null +++ b/v2/pkg/engine/jsonschema/nullable_2020_12_test.go @@ -0,0 +1,103 @@ +package jsonschema + +import ( + "encoding/json" + "testing" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/stretchr/testify/require" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" +) + +// TestNullableFieldsAreJSONSchema2020_12 verifies that the generator expresses +// nullability in the JSON Schema 2020-12 form rather than the OpenAPI 3.0 +// keyword `"nullable": true` (which standard validators silently ignore). +// +// Concretely: a payload that contains explicit `null` values for nullable +// scalar, enum, and recursive-ref fields must validate cleanly against the +// generated schema using a strict standard JSON Schema validator. +func TestNullableFieldsAreJSONSchema2020_12(t *testing.T) { + schemaSDL := scalarDefinitions + ` + schema { query: Query } + + type Query { + processFormula(tree: FormulaNodeInput): Boolean + doThing(input: ThingInput): Boolean + } + + input ThingInput { + name: String + count: Int + rating: Float + active: Boolean + status: Status + } + + enum Status { ACTIVE INACTIVE } + + input FormulaNodeInput { + nodeType: NodeType! + left: FormulaNodeInput + right: FormulaNodeInput + value: Float + } + + enum NodeType { CONSTANT BINARY_OPERATION } + ` + + operationSDL := ` + query Run($tree: FormulaNodeInput, $input: ThingInput) { + processFormula(tree: $tree) + doThing(input: $input) + } + ` + + definitionDoc, report := astparser.ParseGraphqlDocumentString(schemaSDL) + require.False(t, report.HasErrors(), "schema parsing failed: %s", report.Error()) + + operationDoc, report := astparser.ParseGraphqlDocumentString(operationSDL) + require.False(t, report.HasErrors(), "operation parsing failed: %s", report.Error()) + + schema, err := BuildJsonSchema(&operationDoc, &definitionDoc) + require.NoError(t, err) + + schemaJSON, err := json.Marshal(schema) + require.NoError(t, err) + + compiled, err := jsonschema.CompileString("schema.json", string(schemaJSON)) + require.NoError(t, err, "generated JSON schema should compile") + + // Nullable scalars and enum: explicit null values must be accepted. + t.Run("explicit nulls accepted for nullable scalar and enum fields", func(t *testing.T) { + const payloadJSON = `{ + "input": { + "name": null, + "count": null, + "rating": null, + "active": null, + "status": null + } + }` + var payload any + require.NoError(t, json.Unmarshal([]byte(payloadJSON), &payload)) + require.NoError(t, compiled.Validate(payload), + "nullable scalar/enum fields must accept explicit null per JSON Schema 2020-12") + }) + + // Nullable recursive $ref: a leaf may explicitly set left/right to null + // (rather than omitting them) and the schema must accept it. + t.Run("explicit nulls accepted for nullable recursive ref fields", func(t *testing.T) { + const payloadJSON = `{ + "tree": { + "nodeType": "BINARY_OPERATION", + "left": { "nodeType": "CONSTANT", "value": 1, "left": null, "right": null }, + "right": null + } + }` + var payload any + require.NoError(t, json.Unmarshal([]byte(payloadJSON), &payload)) + require.NoError(t, compiled.Validate(payload), + "nullable recursive ref fields must accept explicit null per JSON Schema 2020-12") + }) +} diff --git a/v2/pkg/engine/jsonschema/schema.go b/v2/pkg/engine/jsonschema/schema.go index 21e230274c..0cc72c9730 100644 --- a/v2/pkg/engine/jsonschema/schema.go +++ b/v2/pkg/engine/jsonschema/schema.go @@ -25,7 +25,10 @@ type JsonSchema struct { Required []string `json:"required,omitempty"` AdditionalProperties *bool `json:"additionalProperties,omitempty"` Description string `json:"description,omitempty"` - Nullable bool `json:"nullable,omitempty"` + // Nullable is tracked internally; serialization expresses nullability in the + // JSON Schema 2020-12 form (type-union, anyOf, or null in enum), not the + // OpenAPI 3.0 "nullable" keyword. + Nullable bool `json:"-"` // Ref references a schema defined under the root "$defs" (e.g. "#/$defs/MyInput"). // Used to represent recursive input types, which cannot be inlined. @@ -59,9 +62,19 @@ func (s *JsonSchema) MarshalJSON() ([]byte, error) { // Use a map to only include non-empty fields m := make(map[string]interface{}) + // Nullability is expressed per JSON Schema 2020-12: + // - typed schemas: "type": [, "null"] + // - enum schemas: null appended to the "enum" array + // - $ref schemas: {"anyOf": [{"$ref": ...}, {"type": "null"}]} + // rather than the OpenAPI 3.0 keyword "nullable: true", which standard + // validators ignore. + if s.Type != "" { - // Always use a single type, regardless of nullability - m["type"] = string(s.Type) + if s.Nullable { + m["type"] = []string{string(s.Type), "null"} + } else { + m["type"] = string(s.Type) + } } if len(s.Properties) > 0 { @@ -80,18 +93,21 @@ func (s *JsonSchema) MarshalJSON() ([]byte, error) { m["description"] = s.Description } - // For object types, always include nullable field regardless of value - // For other types, only include nullable when it's true - if s.Type == TypeObject || s.Nullable { - m["nullable"] = s.Nullable - } - if s.Items != nil { m["items"] = s.Items } if len(s.Enum) > 0 { - m["enum"] = s.Enum + if s.Nullable { + enum := make([]any, 0, len(s.Enum)+1) + for _, v := range s.Enum { + enum = append(enum, v) + } + enum = append(enum, nil) + m["enum"] = enum + } else { + m["enum"] = s.Enum + } } if s.Default != nil { @@ -115,7 +131,14 @@ func (s *JsonSchema) MarshalJSON() ([]byte, error) { } if s.Ref != "" { - m["$ref"] = s.Ref + if s.Nullable { + m["anyOf"] = []map[string]string{ + {"$ref": s.Ref}, + {"type": "null"}, + } + } else { + m["$ref"] = s.Ref + } } if len(s.Defs) > 0 { diff --git a/v2/pkg/engine/jsonschema/schema_test.go b/v2/pkg/engine/jsonschema/schema_test.go index 979e7bf7d9..b24f3d43d7 100644 --- a/v2/pkg/engine/jsonschema/schema_test.go +++ b/v2/pkg/engine/jsonschema/schema_test.go @@ -50,62 +50,81 @@ func TestJsonSchema_MarshalJSON(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, + "description": "Test object schema", "properties": { - "name": { - "type": "string", - "description": "A string property", - "default": "default value", - "nullable": true + "address": { + "additionalProperties": false, + "properties": { + "city": { + "type": [ + "string", + "null" + ] + }, + "street": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street" + ], + "type": [ + "object", + "null" + ] }, "age": { - "type": "integer", "minimum": 0, - "nullable": true + "type": [ + "integer", + "null" + ] }, "category": { - "type": "string", "enum": [ "ONE", "TWO", - "THREE" + "THREE", + null ], - "nullable": true + "type": [ + "string", + "null" + ] }, - "address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true - } - }, - "required": [ - "street" - ], - "additionalProperties": false, - "nullable": true + "name": { + "default": "default value", + "description": "A string property", + "type": [ + "string", + "null" + ] }, "tags": { - "type": "array", "items": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, - "nullable": true + "type": [ + "array", + "null" + ] } }, "required": [ "name", "age" ], - "additionalProperties": false, - "description": "Test object schema", - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -142,18 +161,19 @@ func TestJsonSchema_MarshalJSON(t *testing.T) { properties := parsed["properties"].(map[string]interface{}) nestedProp := properties["nested"].(map[string]interface{}) - // Check that it's properly inlined - assert.Equal(t, "object", nestedProp["type"]) + // Check that it's properly inlined; nullable schemas serialize "type" as + // the JSON Schema 2020-12 two-element array [, "null"]. + assert.Equal(t, []any{"object", "null"}, nestedProp["type"]) assert.Equal(t, "Nested schema", nestedProp["description"]) assert.Contains(t, nestedProp, "properties") // Check the array contains the same schema inline itemsProp := properties["items"].(map[string]interface{}) - assert.Equal(t, "array", itemsProp["type"]) + assert.Equal(t, []any{"array", "null"}, itemsProp["type"]) assert.Contains(t, itemsProp, "items") itemsSchema := itemsProp["items"].(map[string]interface{}) - assert.Equal(t, "object", itemsSchema["type"]) + assert.Equal(t, []any{"object", "null"}, itemsSchema["type"]) assert.Equal(t, "Nested schema", itemsSchema["description"]) }) } @@ -170,13 +190,16 @@ func TestSchemaFeatures(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "string", "enum": [ "RED", "GREEN", - "BLUE" + "BLUE", + null ], - "nullable": true + "type": [ + "string", + "null" + ] }` // Compare actual JSON with expected JSON @@ -199,27 +222,35 @@ func TestSchemaFeatures(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { + "age": { + "type": [ + "integer", + "null" + ] + }, "id": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "name": { - "type": "string", - "nullable": true - }, - "age": { - "type": "integer", - "nullable": true + "type": [ + "string", + "null" + ] } }, "required": [ "id", "age" ], - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -241,10 +272,9 @@ func TestSchemaFeatures(t *testing.T) { // Define expected JSON schema for integer expectedIntJSON := `{ - "type": "integer", + "type": ["integer", "null"], "minimum": 0, - "maximum": 100, - "nullable": true + "maximum": 100 }` // Compare actual JSON with expected JSON @@ -260,10 +290,9 @@ func TestSchemaFeatures(t *testing.T) { // Define expected JSON schema for number expectedNumJSON := `{ - "type": "number", + "type": ["number", "null"], "minimum": 0, - "maximum": 100, - "nullable": true + "maximum": 100 }` // Compare actual JSON with expected JSON @@ -356,9 +385,14 @@ func TestSchemaFeatures(t *testing.T) { err = json.Unmarshal(data, &parsed) require.NoError(t, err) - // For string type assertions, we expect the primary type (without null) - typeVal := parsed["type"].(string) - assert.NotEqual(t, "null", typeVal) + // A nullable schema serializes "type" as the JSON Schema 2020-12 two- + // element array [, "null"], not the OpenAPI "nullable: true". + typeArr, ok := parsed["type"].([]interface{}) + require.True(t, ok, "nullable schema should serialize type as an array") + require.Len(t, typeArr, 2) + require.Contains(t, typeArr, "null") + require.NotEqual(t, "null", typeArr[0], + "primary (non-null) type should appear first in the type array") } }) @@ -426,67 +460,88 @@ func TestSchemaFeatures(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, + "description": "User schema with all features", "properties": { - "id": { - "type": "string", - "pattern": "^[a-zA-Z0-9]{8,}$", - "nullable": true + "address": { + "additionalProperties": false, + "properties": { + "city": { + "type": [ + "string", + "null" + ] + }, + "street": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "street" + ], + "type": [ + "object", + "null" + ] + }, + "age": { + "minimum": 13, + "type": [ + "integer", + "null" + ] }, "email": { - "type": "string", - "format": "email", "default": "user@example.com", - "nullable": true + "format": "email", + "type": [ + "string", + "null" + ] }, - "age": { - "type": "integer", - "minimum": 13, - "nullable": true + "id": { + "pattern": "^[a-zA-Z0-9]{8,}$", + "type": [ + "string", + "null" + ] }, "role": { - "type": "string", + "default": "USER", "enum": [ "ADMIN", "USER", - "GUEST" + "GUEST", + null ], - "default": "USER", - "nullable": true + "type": [ + "string", + "null" + ] }, "tags": { - "type": "array", "items": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, - "nullable": true - }, - "address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true - } - }, - "required": [ - "street" - ], - "additionalProperties": false, - "nullable": true + "type": [ + "array", + "null" + ] } }, "required": [ "id" ], - "additionalProperties": false, - "description": "User schema with all features", - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -533,18 +588,24 @@ func TestSchemaFeatures(t *testing.T) { properties := parsed["properties"].(map[string]interface{}) - // Explicitly nullable property should have nullable=true + // Nullability is expressed via the JSON Schema 2020-12 type-union form, + // not the OpenAPI "nullable" keyword (which is no longer emitted). + + // Explicitly nullable property: "type" is the two-element [, "null"] array. nullableProp := properties["nullableString"].(map[string]interface{}) - assert.Equal(t, true, nullableProp["nullable"]) + assert.Equal(t, []interface{}{"string", "null"}, nullableProp["type"]) + _, hasNullableKey := nullableProp["nullable"] + assert.False(t, hasNullableKey, "nullable keyword should not be emitted") - // Non-nullable property should not have nullable field (omitempty) + // Non-nullable property: "type" is a single string and no "nullable" key. nonNullableProp := properties["nonNullableString"].(map[string]interface{}) - _, hasNullable := nonNullableProp["nullable"] - assert.False(t, hasNullable) + assert.Equal(t, "string", nonNullableProp["type"]) + _, hasNullableOnNonNullable := nonNullableProp["nullable"] + assert.False(t, hasNullableOnNonNullable) - // Default property should have nullable=true + // Default (factory-nullable) property: same shape as explicitly nullable. defaultProp := properties["defaultString"].(map[string]interface{}) - assert.Equal(t, true, defaultProp["nullable"]) + assert.Equal(t, []interface{}{"string", "null"}, defaultProp["type"]) // Test WithNullable method schema = NewStringSchema() diff --git a/v2/pkg/engine/jsonschema/variables_schema.go b/v2/pkg/engine/jsonschema/variables_schema.go index da36da60d0..ebd8ba1abe 100644 --- a/v2/pkg/engine/jsonschema/variables_schema.go +++ b/v2/pkg/engine/jsonschema/variables_schema.go @@ -338,7 +338,12 @@ func (v *VariablesSchemaBuilder) ensureDef(typeName string, node ast.Node) { return } v.defs[typeName] = NewObjectSchema() // placeholder to break the recursion - v.defs[typeName] = v.processInputObjectType(node) + body := v.processInputObjectType(node) + // The definition body is the type itself, not nullable; nullability is + // applied per use-site via the "$ref" (rewritten as anyOf-with-null when + // the referencing context is nullable). + body.Nullable = false + v.defs[typeName] = body } // processEnumType processes an enum type definition diff --git a/v2/pkg/engine/jsonschema/variables_schema_test.go b/v2/pkg/engine/jsonschema/variables_schema_test.go index 68046cc142..8fd152be04 100644 --- a/v2/pkg/engine/jsonschema/variables_schema_test.go +++ b/v2/pkg/engine/jsonschema/variables_schema_test.go @@ -82,39 +82,45 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "criteria": { - "type": "object", + "additionalProperties": false, + "description": "Input criteria used to search for employees", "properties": { - "name": { - "type": "string" - }, "department": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "employmentStatus": { - "type": "string", "enum": [ "FULL_TIME", "PART_TIME", "CONTRACTOR", - "INTERN" + "INTERN", + null ], - "nullable": true + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" } }, "required": [ "name" ], - "additionalProperties": false, - "description": "Input criteria used to search for employees", - "nullable": false + "type": "object" } }, - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -190,32 +196,38 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "criteria": { - "type": "object", + "additionalProperties": false, "properties": { "name": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "nested": { - "type": "object", + "additionalProperties": false, "properties": { "hasChildren": { - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "maritalStatus": { - "type": "string", "enum": [ "MARRIED", - "ENGAGED" + "ENGAGED", + null ], - "nullable": true + "type": [ + "string", + "null" + ] }, "nationality": { - "type": "string", "enum": [ "AMERICAN", "DUTCH", @@ -224,28 +236,26 @@ func TestBuildJsonSchema(t *testing.T) { "INDIAN", "SPANISH", "UKRAINIAN" - ] + ], + "type": "string" } }, "required": [ "nationality" ], - "additionalProperties": false, - "nullable": false + "type": "object" } }, "required": [ "nested" ], - "additionalProperties": false, - "nullable": false + "type": "object" } }, "required": [ "criteria" ], - "additionalProperties": false, - "nullable": false + "type": "object" }` // Compare actual JSON with expected JSON @@ -395,26 +405,29 @@ func TestBuildJsonSchema(t *testing.T) { require.True(t, ok) assert.Equal(t, "string", id["type"]) + // Nullable scalars serialize "type" as the JSON Schema 2020-12 two-element + // array [, "null"]. + // Verify includeProfile property includeProfile, ok := properties["includeProfile"].(map[string]interface{}) require.True(t, ok) - assert.Equal(t, "boolean", includeProfile["type"]) + assert.Equal(t, []interface{}{"boolean", "null"}, includeProfile["type"]) assert.Equal(t, true, includeProfile["default"]) // Verify age property age, ok := properties["age"].(map[string]interface{}) require.True(t, ok) - assert.Equal(t, "integer", age["type"]) + assert.Equal(t, []interface{}{"integer", "null"}, age["type"]) // Verify rating property rating, ok := properties["rating"].(map[string]interface{}) require.True(t, ok) - assert.Equal(t, "number", rating["type"]) + assert.Equal(t, []interface{}{"number", "null"}, rating["type"]) // Verify name property name, ok := properties["name"].(map[string]interface{}) require.True(t, ok) - assert.Equal(t, "string", name["type"]) + assert.Equal(t, []interface{}{"string", "null"}, name["type"]) }) t.Run("operation with field descriptions", func(t *testing.T) { @@ -671,130 +684,148 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "input": { - "type": "object", + "additionalProperties": false, + "description": "Level 1 input description", "properties": { "field1": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "nested": { - "type": "object", + "additionalProperties": false, + "description": "Level 2 input description", "properties": { - "field2": { - "type": "boolean", - "nullable": true - }, - "deeper": { - "type": "object", - "properties": { - "field3": { - "type": "number", - "nullable": true - }, - "enumField": { - "type": "string", - "enum": [ - "OPTION_1", - "OPTION_2", - "OPTION_3" - ] - }, - "arrayOfArrays": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - }, - "nullable": true - } - }, - "required": [ - "enumField" - ], - "additionalProperties": false, - "description": "Level 3 input description", - "nullable": false - }, "arrayOfObjects": { - "type": "array", "items": { - "type": "object", + "additionalProperties": false, + "description": "Level 3 input description", "properties": { - "field3": { - "type": "number", - "nullable": true + "arrayOfArrays": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": [ + "array", + "null" + ] }, "enumField": { - "type": "string", "enum": [ "OPTION_1", "OPTION_2", "OPTION_3" - ] + ], + "type": "string" }, - "arrayOfArrays": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - }, - "nullable": true + "field3": { + "type": [ + "number", + "null" + ] } }, "required": [ "enumField" ], - "additionalProperties": false, - "description": "Level 3 input description", - "nullable": true + "type": [ + "object", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "deeper": { + "additionalProperties": false, + "description": "Level 3 input description", + "properties": { + "arrayOfArrays": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": [ + "array", + "null" + ] + }, + "enumField": { + "enum": [ + "OPTION_1", + "OPTION_2", + "OPTION_3" + ], + "type": "string" + }, + "field3": { + "type": [ + "number", + "null" + ] + } }, - "nullable": true + "required": [ + "enumField" + ], + "type": "object" + }, + "field2": { + "type": [ + "boolean", + "null" + ] } }, "required": [ "deeper" ], - "additionalProperties": false, - "description": "Level 2 input description", - "nullable": false + "type": "object" }, "optionalArray": { - "type": "array", "items": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, - "nullable": true + "type": [ + "array", + "null" + ] }, "requiredArray": { - "type": "array", "items": { - "type": "integer", - "nullable": true - } + "type": [ + "integer", + "null" + ] + }, + "type": "array" } }, "required": [ "nested", "requiredArray" ], - "additionalProperties": false, - "description": "Level 1 input description", - "nullable": false + "type": "object" } }, "required": [ "input" ], - "additionalProperties": false, - "nullable": false + "type": "object" }` // Compare actual JSON with expected JSON @@ -1091,61 +1122,74 @@ func TestBuildJsonSchema(t *testing.T) { // Mutually recursive input types (TypeA <-> TypeB) are emitted once each // under "$defs" and referenced via "$ref", so nesting is permitted to any depth. expectedJSON := `{ - "type": "object", - "properties": { - "a": { - "$ref": "#/$defs/TypeA" - } - }, - "required": [ - "a" - ], - "additionalProperties": false, - "nullable": false, "$defs": { "TypeA": { - "type": "object", + "additionalProperties": false, "properties": { + "b": { + "anyOf": [ + { + "$ref": "#/$defs/TypeB" + }, + { + "type": "null" + } + ] + }, "id": { "type": "string" }, "name": { - "type": "string", - "nullable": true - }, - "b": { - "$ref": "#/$defs/TypeB", - "nullable": true + "type": [ + "string", + "null" + ] } }, "required": [ "id" ], - "additionalProperties": false, - "nullable": true + "type": "object" }, "TypeB": { - "type": "object", + "additionalProperties": false, "properties": { - "id": { - "type": "string" + "a": { + "anyOf": [ + { + "$ref": "#/$defs/TypeA" + }, + { + "type": "null" + } + ] }, "description": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, - "a": { - "$ref": "#/$defs/TypeA", - "nullable": true + "id": { + "type": "string" } }, "required": [ "id" ], - "additionalProperties": false, - "nullable": true + "type": "object" } - } + }, + "additionalProperties": false, + "properties": { + "a": { + "$ref": "#/$defs/TypeA" + } + }, + "required": [ + "a" + ], + "type": "object" }` // Compare actual JSON with expected JSON @@ -1213,56 +1257,34 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "input": { - "type": "object", + "additionalProperties": false, "properties": { + "age": { + "type": [ + "integer", + "null" + ] + }, "id": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "name": { "type": "string" }, - "age": { - "type": "integer", - "nullable": true - }, - "tags": { - "type": "array", - "items": { - "type": "string", - "nullable": true - }, - "nullable": true - }, - "requiredTags": { - "type": "array", - "items": { - "type": "string", - "nullable": true - } - }, - "nonNullTags": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "requiredNonNullTags": { - "type": "array", - "items": { - "type": "string" - } - }, "nested": { - "type": "object", + "additionalProperties": false, "properties": { "field": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "requiredField": { "type": "string" @@ -1271,15 +1293,28 @@ func TestBuildJsonSchema(t *testing.T) { "required": [ "requiredField" ], - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] + }, + "nonNullTags": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] }, "requiredNested": { - "type": "object", + "additionalProperties": false, "properties": { "field": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "requiredField": { "type": "string" @@ -1288,8 +1323,34 @@ func TestBuildJsonSchema(t *testing.T) { "required": [ "requiredField" ], - "additionalProperties": false, - "nullable": false + "type": "object" + }, + "requiredNonNullTags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "requiredTags": { + "items": { + "type": [ + "string", + "null" + ] + }, + "type": "array" + }, + "tags": { + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] } }, "required": [ @@ -1298,12 +1359,13 @@ func TestBuildJsonSchema(t *testing.T) { "requiredNonNullTags", "requiredNested" ], - "additionalProperties": false, - "nullable": false + "type": "object" } }, - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -1364,7 +1426,7 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema for required argument case expectedJSON1 := `{ - "type": "object", + "additionalProperties": false, "properties": { "id": { "type": "string" @@ -1373,8 +1435,7 @@ func TestBuildJsonSchema(t *testing.T) { "required": [ "id" ], - "additionalProperties": false, - "nullable": false + "type": "object" }` // Compare actual JSON with expected JSON @@ -1393,15 +1454,19 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema for optional argument case expectedJSON2 := `{ - "type": "object", + "additionalProperties": false, "properties": { "name": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } }, - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -1467,26 +1532,31 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "criteria": { - "type": "object", + "additionalProperties": false, "properties": { - "name": { - "type": "string", - "nullable": true - }, "department": { - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] } }, - "additionalProperties": false, - "nullable": false + "type": "object" } }, - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -1545,19 +1615,19 @@ func TestBuildJsonSchema(t *testing.T) { // Define expected JSON schema expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { - "from": { - "nullable": true, - "description": "ISO-8601 date time format" - }, "filter": { - "nullable": true, "description": "JSON object represented as string" + }, + "from": { + "description": "ISO-8601 date time format" } }, - "additionalProperties": false, - "nullable": true + "type": [ + "object", + "null" + ] }` // Compare actual JSON with expected JSON @@ -1608,18 +1678,17 @@ func TestBuildJsonSchema(t *testing.T) { require.NoError(t, err) expectedJSON := `{ - "type": "object", + "additionalProperties": false, "properties": { "id": { - "type": "string", - "description": "The unique employee identifier" + "description": "The unique employee identifier", + "type": "string" } }, "required": [ "id" ], - "additionalProperties": false, - "nullable": false + "type": "object" }` assert.JSONEq(t, expectedJSON, string(data))