Skip to content

Commit 6c01290

Browse files
openapi3: enforce unique required entries and unique tag names in Validate() (getkin#1203)
* openapi3: enforce unique required entries and unique tag names in Validate() Two spec MUSTs that Validate() did not check (getkin#1200): - The elements of a schema's `required` array MUST be unique (JSON Schema 2020-12 §6.5.3 for OAS 3.1, draft-04 for OAS 3.0). schema.validate now returns DuplicateRequiredFieldError on a repeat. - Each tag name in the document-root `tags` list MUST be unique (OpenAPI Object). Tags.Validate now emits DuplicateTagError on a repeat. Both follow the existing Duplicate* cluster pattern (cf. DuplicateParameterError) and carry Origin when the document was loaded with IncludeOrigin. This is a behavior change: documents carrying these duplicates that passed Validate() before now fail. No apis-guru golden fixture changed (the corpus trips neither check). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * openapi3: regenerate .github/docs for the new error types go doc dump (docs.sh) now lists DuplicateRequiredFieldError and DuplicateTagError, which the CI `git diff --exit-code` gate after generation requires to be committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * openapi3: drop redundant else in tag uniqueness check Re-inserting an existing key into the seen set is a no-op, so the seen update can run unconditionally after the duplicate check instead of in an else. No behavior change. (review feedback) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2222e35 commit 6c01290

5 files changed

Lines changed: 118 additions & 0 deletions

File tree

.github/docs/openapi3.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,31 @@ type DuplicateParameterError struct {
608608

609609
func (e *DuplicateParameterError) Error() string
610610

611+
type DuplicateRequiredFieldError struct {
612+
// Field is the field name listed more than once in `required`.
613+
Field string
614+
// Origin is the source location of the offending schema when the
615+
// document was loaded with Loader.IncludeOrigin = true.
616+
Origin *Origin
617+
}
618+
DuplicateRequiredFieldError clusters "duplicate field in required" failures.
619+
The elements of a schema's `required` array MUST be unique (JSON Schema
620+
2020-12 §6.5.3 for OpenAPI 3.1, draft-04 for OpenAPI 3.0).
621+
622+
func (e *DuplicateRequiredFieldError) Error() string
623+
624+
type DuplicateTagError struct {
625+
// Name is the tag name that appears more than once.
626+
Name string
627+
// Origin is the source location of the offending tag when the document
628+
// was loaded with Loader.IncludeOrigin = true.
629+
Origin *Origin
630+
}
631+
DuplicateTagError clusters "more than one tag has name X" failures. Each tag
632+
name in the document-root `tags` list MUST be unique (OpenAPI Object).
633+
634+
func (e *DuplicateTagError) Error() string
635+
611636
type DynamicAnchorFieldFor31Plus struct{ ValidationError }
612637

613638
func (e *DynamicAnchorFieldFor31Plus) As(target any) bool

openapi3/schema.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,18 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema,
14061406
return stack, newSchemaReadOnlyWriteOnlyExclusive(schema.Origin)
14071407
}
14081408

1409+
// The elements of `required` MUST be unique (JSON Schema 2020-12 §6.5.3
1410+
// for OAS 3.1, draft-04 for OAS 3.0).
1411+
if len(schema.Required) > 1 {
1412+
seen := make(map[string]struct{}, len(schema.Required))
1413+
for _, name := range schema.Required {
1414+
if _, dup := seen[name]; dup {
1415+
return stack, &DuplicateRequiredFieldError{Field: name, Origin: schema.Origin}
1416+
}
1417+
seen[name] = struct{}{}
1418+
}
1419+
}
1420+
14091421
// Reject fields that only exist in OAS 3.1 / JSON Schema 2020-12 when the
14101422
// document is OAS 3.0. Fields explicitly allowed via AllowExtraSiblingFields
14111423
// are skipped (opt-in escape hatch for 3.0 docs that reference external

openapi3/tag.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,19 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error {
2323
ctx = WithValidationOptions(ctx, opts...)
2424
me := newErrCollector(ctx)
2525

26+
// Each tag name in the list MUST be unique (OpenAPI Object). Empty names
27+
// are left to per-tag validation, not flagged as duplicates here.
28+
seen := make(map[string]struct{}, len(tags))
29+
2630
for _, v := range tags {
31+
if v.Name != "" {
32+
if _, dup := seen[v.Name]; dup {
33+
if err := me.emit(&DuplicateTagError{Name: v.Name, Origin: v.Origin}); err != nil {
34+
return err
35+
}
36+
}
37+
seen[v.Name] = struct{}{}
38+
}
2739
wrap := func(e error) error { return &TagValidationError{Name: v.Name, Cause: e} }
2840
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
2941
return err

openapi3/validation_error.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,35 @@ func (e *DuplicateParameterError) Error() string {
535535
return fmt.Sprintf("more than one %q parameter has name %q", e.In, e.Name)
536536
}
537537

538+
// DuplicateRequiredFieldError clusters "duplicate field in required" failures.
539+
// The elements of a schema's `required` array MUST be unique (JSON Schema
540+
// 2020-12 §6.5.3 for OpenAPI 3.1, draft-04 for OpenAPI 3.0).
541+
type DuplicateRequiredFieldError struct {
542+
// Field is the field name listed more than once in `required`.
543+
Field string
544+
// Origin is the source location of the offending schema when the
545+
// document was loaded with Loader.IncludeOrigin = true.
546+
Origin *Origin
547+
}
548+
549+
func (e *DuplicateRequiredFieldError) Error() string {
550+
return fmt.Sprintf("duplicate field %q in required", e.Field)
551+
}
552+
553+
// DuplicateTagError clusters "more than one tag has name X" failures. Each tag
554+
// name in the document-root `tags` list MUST be unique (OpenAPI Object).
555+
type DuplicateTagError struct {
556+
// Name is the tag name that appears more than once.
557+
Name string
558+
// Origin is the source location of the offending tag when the document
559+
// was loaded with Loader.IncludeOrigin = true.
560+
Origin *Origin
561+
}
562+
563+
func (e *DuplicateTagError) Error() string {
564+
return fmt.Sprintf("more than one tag has name %q", e.Name)
565+
}
566+
538567
// InvalidSerializationMethodError clusters "serialization method with
539568
// style=X and explode=Y is not supported by Z" failures. Fires for
540569
// invalid (style, explode) combinations on encodings, parameters,

openapi3/validation_error_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,46 @@ func TestValidationError_SchemaCombinatorElementValidationError_NoStutter(t *tes
21272127
require.Equal(t, "invalid oneOf element: invalid allOf element: boom", mixed.Error())
21282128
}
21292129

2130+
func TestValidationError_DuplicateRequiredFieldError(t *testing.T) {
2131+
doc := loadDocFromYAML(t, `
2132+
openapi: 3.0.3
2133+
info: { title: t, version: "1" }
2134+
paths: {}
2135+
components:
2136+
schemas:
2137+
Bad:
2138+
type: object
2139+
required: [id, id]
2140+
properties:
2141+
id: { type: string }
2142+
`)
2143+
err := doc.Validate(context.Background())
2144+
require.Error(t, err)
2145+
2146+
var dup *openapi3.DuplicateRequiredFieldError
2147+
require.True(t, errors.As(err, &dup))
2148+
require.Equal(t, "id", dup.Field)
2149+
require.Contains(t, dup.Error(), `duplicate field "id" in required`)
2150+
}
2151+
2152+
func TestValidationError_DuplicateTagError(t *testing.T) {
2153+
doc := loadDocFromYAML(t, `
2154+
openapi: 3.0.3
2155+
info: { title: t, version: "1" }
2156+
paths: {}
2157+
tags:
2158+
- name: pet
2159+
- name: pet
2160+
`)
2161+
err := doc.Validate(context.Background())
2162+
require.Error(t, err)
2163+
2164+
var dup *openapi3.DuplicateTagError
2165+
require.True(t, errors.As(err, &dup))
2166+
require.Equal(t, "pet", dup.Name)
2167+
require.Contains(t, dup.Error(), `more than one tag has name "pet"`)
2168+
}
2169+
21302170
func TestValidationError_TagValidationError(t *testing.T) {
21312171
doc := loadDocFromYAML(t, `
21322172
openapi: 3.0.3

0 commit comments

Comments
 (0)