Skip to content

Commit db6574e

Browse files
authored
Openapi v3 fix duplicates in enums (#2168)
* fix(openapi3): preserve ref-backed enum schemas (cherry picked from commit cd3d63b) * fix(openapi3): avoid mutating array item ref schemas (cherry picked from commit f7b7fcd) * fix(openapi3): emit ref siblings for field metadata (cherry picked from commit b75ef90)
1 parent 575d84f commit db6574e

2 files changed

Lines changed: 134 additions & 17 deletions

File tree

field_parser_v3_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,79 @@ func TestDefaultFieldParserV3(t *testing.T) {
240240

241241
})
242242

243+
t.Run("Enums tag on ref schema does not mutate component schema", func(t *testing.T) {
244+
t.Parallel()
245+
246+
parser := New()
247+
parser.openAPIVersion = true
248+
249+
componentSchema := spec.NewSchemaSpec()
250+
componentSchema.Spec.Type = &spec.SingleOrArray[string]{STRING}
251+
componentSchema.Spec.Enum = []interface{}{"image", "video"}
252+
componentSchema.Spec.Extensions = map[string]any{
253+
enumVarNamesExtension: []string{"PostTypeImage", "PostTypeVideo"},
254+
}
255+
parser.openAPI.Components.Spec.Schemas = map[string]*spec.RefOrSpec[spec.Schema]{
256+
"PostType": componentSchema,
257+
}
258+
259+
schema := spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/PostType"), nil)
260+
261+
err := newTagBaseFieldParserV3(
262+
parser,
263+
&ast.File{Name: &ast.Ident{Name: "test"}},
264+
&ast.Field{
265+
Names: []*ast.Ident{{Name: "Type"}},
266+
Tag: &ast.BasicLit{
267+
Value: `json:"type" validate:"oneof=video image" example:"video"`,
268+
},
269+
},
270+
).ComplementSchema(schema)
271+
assert.NoError(t, err)
272+
273+
assert.Equal(t, []interface{}{"image", "video"}, componentSchema.Spec.Enum)
274+
assert.Equal(t, []string{"PostTypeImage", "PostTypeVideo"}, componentSchema.Spec.Extensions[enumVarNamesExtension])
275+
if assert.NotNil(t, schema.Spec) {
276+
assert.Equal(t, "video", schema.Spec.Example)
277+
assert.Equal(t, "#/components/schemas/PostType", schema.Spec.Extensions["$ref"])
278+
assert.Empty(t, schema.Spec.Enum)
279+
}
280+
})
281+
282+
t.Run("Enums tag on array item ref schema does not mutate component schema", func(t *testing.T) {
283+
t.Parallel()
284+
285+
parser := New()
286+
parser.openAPIVersion = true
287+
288+
componentSchema := spec.NewSchemaSpec()
289+
componentSchema.Spec.Type = &spec.SingleOrArray[string]{STRING}
290+
componentSchema.Spec.Enum = []interface{}{"image", "video"}
291+
parser.openAPI.Components.Spec.Schemas = map[string]*spec.RefOrSpec[spec.Schema]{
292+
"PostType": componentSchema,
293+
}
294+
295+
schema := spec.NewSchemaSpec()
296+
schema.Spec.Type = &spec.SingleOrArray[string]{ARRAY}
297+
schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/PostType"), nil))
298+
299+
err := newTagBaseFieldParserV3(
300+
parser,
301+
&ast.File{Name: &ast.Ident{Name: "test"}},
302+
&ast.Field{
303+
Names: []*ast.Ident{{Name: "Types"}},
304+
Tag: &ast.BasicLit{
305+
Value: `json:"types" validate:"oneof=video image"`,
306+
},
307+
},
308+
).ComplementSchema(schema)
309+
assert.NoError(t, err)
310+
311+
assert.Equal(t, []interface{}{"image", "video"}, componentSchema.Spec.Enum)
312+
assert.Equal(t, "#/components/schemas/PostType", schema.Spec.Items.Schema.Ref.Ref)
313+
assert.Nil(t, schema.Spec.Items.Schema.Spec)
314+
})
315+
243316
t.Run("EnumVarNames tag", func(t *testing.T) {
244317
t.Parallel()
245318

field_parserv3.go

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,38 +117,32 @@ func (ps *tagBaseFieldParserV3) CustomSchema() (*spec.RefOrSpec[spec.Schema], er
117117

118118
// ComplementSchema complement schema with field properties
119119
func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error {
120-
if schema.Spec == nil {
121-
componentSchema := ps.p.openAPI.Components.Spec.Schemas[strings.ReplaceAll(schema.Ref.Ref, "#/components/schemas/", "")]
122-
if componentSchema == nil {
123-
return fmt.Errorf("could not resolve schema for ref %s", schema.Ref.Ref)
124-
}
125-
schema = componentSchema
126-
}
127-
128120
types := ps.p.GetSchemaTypePathV3(schema, 2)
129121
if len(types) == 0 {
130122
return fmt.Errorf("invalid type for field: %s", ps.field.Names[0])
131123
}
132124

133125
if schema.Ref != nil { //IsRefSchema(schema)
134-
// TODO fetch existing schema from components
135126
var newSchema = spec.Schema{}
136-
err := ps.complementSchema(&newSchema, types)
127+
err := ps.complementSchema(&newSchema, types, ps.p.getSchemaByRef(schema.Ref))
137128
if err != nil {
138129
return err
139130
}
140131
if !reflect.ValueOf(newSchema).IsZero() {
141-
newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}}
132+
if newSchema.Extensions == nil {
133+
newSchema.Extensions = make(map[string]any)
134+
}
135+
newSchema.Extensions["$ref"] = schema.Ref.Ref
142136
*schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema}
143137
}
144138
return nil
145139
}
146140

147-
return ps.complementSchema(schema.Spec, types)
141+
return ps.complementSchema(schema.Spec, types, nil)
148142
}
149143

150144
// complementSchema complement schema with field properties
151-
func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []string) error {
145+
func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []string, refSchema *spec.Schema) error {
152146
if ps.field.Tag == nil {
153147
if ps.field.Doc != nil {
154148
schema.Description = strings.TrimSpace(ps.field.Doc.Text())
@@ -357,16 +351,22 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
357351
}
358352

359353
elemSchema := schema
354+
elemRefSchema := refSchema
355+
var itemRef *spec.Ref
360356

361357
if field.schemaType == ARRAY {
362358
// For Array only
363359
schema.MaxItems = field.maxItems
364360
schema.MinItems = field.minItems
365361
schema.UniqueItems = &field.unique
366362

367-
elemSchema = schema.Items.Schema.Spec
368-
if elemSchema == nil {
369-
elemSchema = ps.p.getSchemaByRef(schema.Items.Schema.Ref)
363+
if schema.Items.Schema.Ref != nil {
364+
itemRef = schema.Items.Schema.Ref
365+
elemSchema = &spec.Schema{}
366+
elemRefSchema = ps.p.getSchemaByRef(itemRef)
367+
} else {
368+
elemSchema = schema.Items.Schema.Spec
369+
elemRefSchema = nil
370370
}
371371

372372
elemSchema.Format = field.formatType
@@ -377,13 +377,57 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
377377
elemSchema.MultipleOf = field.multipleOf
378378
elemSchema.MaxLength = field.maxLength
379379
elemSchema.MinLength = field.minLength
380-
elemSchema.Enum = append(elemSchema.Enum, field.enums...)
380+
if shouldApplyFieldEnumConstraint(field.enums, elemRefSchema) {
381+
elemSchema.Enum = append(elemSchema.Enum, field.enums...)
382+
}
381383
elemSchema.Pattern = field.pattern
382384
elemSchema.OneOf = oneOfSchemas
383385

386+
if itemRef != nil && !reflect.ValueOf(*elemSchema).IsZero() {
387+
if elemSchema.Extensions == nil {
388+
elemSchema.Extensions = make(map[string]any)
389+
}
390+
elemSchema.Extensions["$ref"] = itemRef.Ref
391+
schema.Items.Schema = spec.NewRefOrSpec[spec.Schema](nil, elemSchema)
392+
}
393+
384394
return nil
385395
}
386396

397+
func shouldApplyFieldEnumConstraint(fieldEnums []interface{}, refSchema *spec.Schema) bool {
398+
if len(fieldEnums) == 0 {
399+
return false
400+
}
401+
402+
if refSchema == nil || len(refSchema.Enum) == 0 {
403+
return true
404+
}
405+
406+
if len(fieldEnums) != len(refSchema.Enum) {
407+
return true
408+
}
409+
410+
used := make([]bool, len(refSchema.Enum))
411+
for _, fieldEnum := range fieldEnums {
412+
matched := false
413+
for i, refEnum := range refSchema.Enum {
414+
if used[i] {
415+
continue
416+
}
417+
if reflect.DeepEqual(fieldEnum, refEnum) {
418+
used[i] = true
419+
matched = true
420+
break
421+
}
422+
}
423+
if !matched {
424+
return true
425+
}
426+
}
427+
428+
return false
429+
}
430+
387431
func getIntTagV3(structTag reflect.StructTag, tagName string) (*int, error) {
388432
strValue := structTag.Get(tagName)
389433
if strValue == "" {

0 commit comments

Comments
 (0)