Skip to content

Commit 70e824b

Browse files
committed
fix(validator): reject type "null" in OAS 3.0 documents
In OAS 3.0.x, the only valid schema types are array, boolean, integer, number, object, and string. The "null" type was introduced in OAS 3.1+ (JSON Schema 2020-12). Previously, validateSchemaTypeConstraints silently accepted type: "null" on 3.0 documents because its switch had no "null" case and no version check. This change plumbs the parsed OAS version onto the Validator via a new oasVersion field (set in ValidateParsed) and adds a "null" case to the schema type switch that emits an error when the document is OAS 3.0.x. OAS 3.1+ continues to accept both scalar type: "null" and the type-array form (e.g. ["string", "null"]). Fixes #362
1 parent 91814fe commit 70e824b

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

validator/schema.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,30 @@ func (v *Validator) validateSchemaTypeConstraints(schema *parser.Schema, path st
170170
withSpecRef(getJSONSchemaRef()),
171171
)
172172
}
173+
case "null":
174+
// "null" is only a valid JSON Schema type in OAS 3.1+ (JSON Schema 2020-12).
175+
// In OAS 3.0.x, the only valid types are: array, boolean, integer, number,
176+
// object, string. Nullability is expressed via "nullable: true".
177+
// Note: this only catches the scalar form (schema.Type == "null"). OAS 3.1+
178+
// type arrays (e.g. ["string", "null"]) are represented as []any and bypass
179+
// this switch entirely, which is the correct behavior.
180+
if isOAS30x(v.oasVersion) {
181+
v.addError(result, path,
182+
`"null" is not a valid type for OpenAPI 3.0; valid types are: array, boolean, integer, number, object, string. Use "nullable: true" instead.`,
183+
withSpecRef("https://spec.openapis.org/oas/v3.0.0.html#data-types"),
184+
withField("type"),
185+
withValue("null"),
186+
)
187+
}
173188
}
174189
}
175190

191+
// isOAS30x reports whether the given version is in the OAS 3.0.x family,
192+
// where "null" is not a valid schema type.
193+
func isOAS30x(version parser.OASVersion) bool {
194+
return version >= parser.OASVersion300 && version <= parser.OASVersion304
195+
}
196+
176197
// validateRequiredFields validates that required fields exist in properties
177198
func (v *Validator) validateRequiredFields(schema *parser.Schema, path string, result *ValidationResult) {
178199
for _, reqField := range schema.Required {

validator/schema_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,148 @@ func TestValidate_WhitespaceSchemaName_OAS3(t *testing.T) {
413413
assert.True(t, foundError, "Should have error about whitespace-only schema name")
414414
}
415415

416+
// ========================================
417+
// Schema Type "null" Validation Tests (OAS 3.0 vs 3.1+)
418+
// ========================================
419+
420+
// TestValidate_NullType_OAS30_Rejected verifies that schemas declaring
421+
// type: "null" are rejected in OAS 3.0.x, where the only valid types are
422+
// array, boolean, integer, number, object, string. Nullability in 3.0 is
423+
// expressed via "nullable: true". See issue #362.
424+
func TestValidate_NullType_OAS30_Rejected(t *testing.T) {
425+
tests := []struct {
426+
name string
427+
version string
428+
oasVersion parser.OASVersion
429+
}{
430+
{"OAS 3.0.0", "3.0.0", parser.OASVersion300},
431+
{"OAS 3.0.1", "3.0.1", parser.OASVersion301},
432+
{"OAS 3.0.2", "3.0.2", parser.OASVersion302},
433+
{"OAS 3.0.3", "3.0.3", parser.OASVersion303},
434+
{"OAS 3.0.4", "3.0.4", parser.OASVersion304},
435+
}
436+
437+
for _, tt := range tests {
438+
t.Run(tt.name, func(t *testing.T) {
439+
doc := &parser.OAS3Document{
440+
OpenAPI: tt.version,
441+
Info: &parser.Info{
442+
Title: "Test API",
443+
Version: "1.0.0",
444+
},
445+
Paths: map[string]*parser.PathItem{},
446+
Components: &parser.Components{
447+
Schemas: map[string]*parser.Schema{
448+
"BadNull": {
449+
Type: "null",
450+
},
451+
},
452+
},
453+
}
454+
455+
parseResult := &parser.ParseResult{
456+
Version: tt.version,
457+
OASVersion: tt.oasVersion,
458+
Document: doc,
459+
}
460+
461+
v := New()
462+
result, err := v.ValidateParsed(*parseResult)
463+
require.NoError(t, err)
464+
465+
foundNullTypeError := false
466+
for _, e := range result.Errors {
467+
if strings.Contains(e.Path, "components.schemas.BadNull") &&
468+
strings.Contains(e.Message, `"null" is not a valid type for OpenAPI 3.0`) {
469+
foundNullTypeError = true
470+
assert.Equal(t, "type", e.Field, "Error should set Field to 'type'")
471+
assert.Equal(t, "null", e.Value, "Error should set Value to 'null'")
472+
assert.Contains(t, e.SpecRef, "spec.openapis.org/oas/v3.0.0.html#data-types",
473+
"Error should reference OAS 3.0 data-types spec section")
474+
break
475+
}
476+
}
477+
assert.True(t, foundNullTypeError,
478+
"Expected error about 'null' not being a valid OAS 3.0 type, got errors: %v",
479+
result.Errors)
480+
assert.False(t, result.Valid, "Document should be invalid")
481+
})
482+
}
483+
}
484+
485+
// TestValidate_NullType_OAS31_Allowed verifies that schemas declaring
486+
// type: "null" are accepted in OAS 3.1+ (JSON Schema 2020-12).
487+
func TestValidate_NullType_OAS31_Allowed(t *testing.T) {
488+
doc := &parser.OAS3Document{
489+
OpenAPI: "3.1.0",
490+
Info: &parser.Info{
491+
Title: "Test API",
492+
Version: "1.0.0",
493+
},
494+
Paths: map[string]*parser.PathItem{},
495+
Components: &parser.Components{
496+
Schemas: map[string]*parser.Schema{
497+
"NullSchema": {
498+
Type: "null",
499+
},
500+
},
501+
},
502+
}
503+
504+
parseResult := &parser.ParseResult{
505+
Version: "3.1.0",
506+
OASVersion: parser.OASVersion310,
507+
Document: doc,
508+
}
509+
510+
v := New()
511+
result, err := v.ValidateParsed(*parseResult)
512+
require.NoError(t, err)
513+
514+
// No error about "null" being invalid should be present under OAS 3.1.
515+
for _, e := range result.Errors {
516+
if strings.Contains(e.Message, `"null" is not a valid type for OpenAPI 3.0`) {
517+
t.Errorf("Unexpected OAS 3.0 null-type error under OAS 3.1: %s", e.String())
518+
}
519+
}
520+
}
521+
522+
// TestValidate_StringType_OAS30_Allowed is a regression guard ensuring that
523+
// the OAS 3.0 null-type rejection does not flag other legitimate types.
524+
func TestValidate_StringType_OAS30_Allowed(t *testing.T) {
525+
doc := &parser.OAS3Document{
526+
OpenAPI: "3.0.0",
527+
Info: &parser.Info{
528+
Title: "Test API",
529+
Version: "1.0.0",
530+
},
531+
Paths: map[string]*parser.PathItem{},
532+
Components: &parser.Components{
533+
Schemas: map[string]*parser.Schema{
534+
"Plain": {
535+
Type: "string",
536+
},
537+
},
538+
},
539+
}
540+
541+
parseResult := &parser.ParseResult{
542+
Version: "3.0.0",
543+
OASVersion: parser.OASVersion300,
544+
Document: doc,
545+
}
546+
547+
v := New()
548+
result, err := v.ValidateParsed(*parseResult)
549+
require.NoError(t, err)
550+
551+
for _, e := range result.Errors {
552+
if strings.Contains(e.Path, "components.schemas.Plain") {
553+
t.Errorf("Unexpected error for valid string schema under OAS 3.0: %s", e.String())
554+
}
555+
}
556+
}
557+
416558
// TestValidate_ValidSchemaNames tests that valid schema names pass validation
417559
func TestValidate_ValidSchemaNames(t *testing.T) {
418560
tests := []struct {

validator/validator.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ type Validator struct {
124124
// refTracker tracks which operations reference which components.
125125
// Built during ValidateParsed for populating OperationContext on issues.
126126
refTracker *refTracker
127+
// oasVersion is the detected OAS version for the document under validation.
128+
// Set during ValidateParsed so version-sensitive checks (e.g., type "null"
129+
// being illegal in OAS 3.0.x) can consult it without plumbing through every call.
130+
oasVersion parser.OASVersion
127131
}
128132

129133
// New creates a new Validator instance with default settings
@@ -260,6 +264,10 @@ func (v *Validator) ValidateParsed(parseResult parser.ParseResult) (*ValidationR
260264
SourcePath: parseResult.SourcePath,
261265
}
262266

267+
// Record the OAS version for version-sensitive checks (e.g., type "null"
268+
// being illegal in OAS 3.0.x but valid in OAS 3.1+).
269+
v.oasVersion = parseResult.OASVersion
270+
263271
// Build reference tracker for operation context
264272
switch doc := parseResult.Document.(type) {
265273
case *parser.OAS3Document:

0 commit comments

Comments
 (0)