Repository: swaggo/swag
Pull request #1870 (2024-08-13 17:10:03)

Feat openapiv3 oneof
**Describe the PR**
Adds support for the oneOf feature in OpenAPI v3, allowing you to describe a field/response that can have multiple types.

**Relation issue**
https://github.com/swaggo/swag/issues/1363
https://github.com/swaggo/swag/issues/1617

**Additional context**
Unfortunately, the PR https://github.com/swaggo/swag/pull/1671 was reverted due to a compile error caused by the use of the `maps` package. 
The `maps` package was introduced in Go 1.21, but the project is currently using Go 1.18.


ubogdan: Please rebase from V2, we updated the go version and it's very likely to work now.

Patch:
diff --git a/field_parserv3.go b/field_parserv3.go
index 733907f..37a9c64 100644
--- a/field_parserv3.go
+++ b/field_parserv3.go
@@ -83,13 +83,15 @@ func (sf *structFieldV3) setMax(valValue string) {
 
 type tagBaseFieldParserV3 struct {
 	p     *Parser
+	file  *ast.File
 	field *ast.Field
 	tag   reflect.StructTag
 }
 
-func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 {
+func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 {
 	fieldParser := tagBaseFieldParserV3{
 		p:     p,
+		file:  file,
 		field: field,
 		tag:   "",
 	}
@@ -134,9 +136,10 @@ func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Sch
 		if err != nil {
 			return err
 		}
-		// if !reflect.ValueOf(newSchema).IsZero() {
-		// 	*schema = *(newSchema.WithAllOf(*schema.Spec))
-		// }
+		if !reflect.ValueOf(newSchema).IsZero() {
+			newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}}
+			*schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema}
+		}
 		return nil
 	}
 
@@ -339,6 +342,19 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
 		}
 	}
 
+	var oneOfSchemas []*spec.RefOrSpec[spec.Schema]
+	oneOfTagValue := ps.tag.Get(oneOfTag)
+	if oneOfTagValue != "" {
+		oneOfTypes := strings.Split((oneOfTagValue), ",")
+		for _, oneOfType := range oneOfTypes {
+			oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true)
+			if err != nil {
+				return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err)
+			}
+			oneOfSchemas = append(oneOfSchemas, oneOfSchema)
+		}
+	}
+
 	elemSchema := schema
 
 	if field.schemaType == ARRAY {
@@ -362,6 +378,7 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st
 	elemSchema.MinLength = field.minLength
 	elemSchema.Enum = field.enums
 	elemSchema.Pattern = field.pattern
+	elemSchema.OneOf = oneOfSchemas
 
 	return nil
 }
diff --git a/operation.go b/operation.go
index c012787..12fd7a0 100644
--- a/operation.go
+++ b/operation.go
@@ -435,6 +435,7 @@ const (
 	extensionsTag       = "extensions"
 	collectionFormatTag = "collectionFormat"
 	patternTag          = "pattern"
+	oneOfTag            = "oneOf"
 )
 
 var regexAttributes = map[string]*regexp.Regexp{
diff --git a/operationv3.go b/operationv3.go
index 4737694..5d94f10 100644
--- a/operationv3.go
+++ b/operationv3.go
@@ -926,22 +926,15 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File
 
 	for _, codeStr := range strings.Split(matches[1], ",") {
 		if strings.EqualFold(codeStr, defaultTag) {
-			response := o.DefaultResponse()
-			response.Description = description
-
-			mimeType := "application/json" // TODO: set correct mimeType
-			setResponseSchema(response, mimeType, schema)
-
-			continue
-		}
-
-		code, err := strconv.Atoi(codeStr)
-		if err != nil {
-			return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
-		}
-
-		if description == "" {
-			description = http.StatusText(code)
+			codeStr = ""
+		} else {
+			code, err := strconv.Atoi(codeStr)
+			if err != nil {
+				return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
+			}
+			if description == "" {
+				description = http.StatusText(code)
+			}
 		}
 
 		response := spec.NewResponseSpec()
@@ -979,15 +972,12 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error {
 
 	for _, codeStr := range strings.Split(matches[1], ",") {
 		if strings.EqualFold(codeStr, defaultTag) {
-			response := o.DefaultResponse()
-			response.Description = description
-
-			continue
-		}
-
-		_, err := strconv.Atoi(codeStr)
-		if err != nil {
-			return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
+			codeStr = ""
+		} else {
+			_, err := strconv.Atoi(codeStr)
+			if err != nil {
+				return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
+			}
 		}
 
 		o.AddResponse(codeStr, newResponseWithDescription(description))
@@ -996,21 +986,10 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error {
 	return nil
 }
 
-// DefaultResponse return the default response member pointer.
-func (o *OperationV3) DefaultResponse() *spec.Response {
-	if o.Responses.Spec.Default == nil {
-		o.Responses.Spec.Default = spec.NewResponseSpec()
-		o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
-	}
-
-	if o.Responses.Spec.Default.Spec.Spec.Content == nil {
-		o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType])
-	}
-
-	return o.Responses.Spec.Default.Spec.Spec
-}
-
 // AddResponse add a response for a code.
+// If the code is already exist, it will merge with the old one:
+// 1. The description will be replaced by the new one if the new one is not empty.
+// 2. The content schema will be merged using `oneOf` if the new one is not empty.
 func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) {
 	if response.Spec.Spec.Headers == nil {
 		response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]])
@@ -1020,24 +999,78 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext
 		o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]])
 	}
 
-	o.Responses.Spec.Response[code] = response
+	res := response
+	var prev *spec.RefOrSpec[spec.Extendable[spec.Response]]
+	if code != "" {
+		prev = o.Responses.Spec.Response[code]
+	} else {
+		prev = o.Responses.Spec.Default
+	}
+	if prev != nil { // merge into prev
+		res = prev
+		if response.Spec.Spec.Description != "" {
+			prev.Spec.Spec.Description = response.Spec.Spec.Description
+		}
+		if len(response.Spec.Spec.Content) > 0 {
+			// responses should only have one content type
+			singleKey := ""
+			for k := range response.Spec.Spec.Content {
+				singleKey = k
+				break
+			}
+			if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil {
+				prev.Spec.Spec.Content = response.Spec.Spec.Content
+			} else {
+				newMediaType := response.Spec.Spec.Content[singleKey]
+				if len(newMediaType.Extensions) > 0 {
+					if prevMediaType.Extensions == nil {
+						prevMediaType.Extensions = make(map[string]interface{})
+					}
+					for k, v := range newMediaType.Extensions {
+						prevMediaType.Extensions[k] = v
+					}
+				}
+				if len(newMediaType.Spec.Examples) > 0 {
+					if prevMediaType.Spec.Examples == nil {
+						prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]])
+					}
+					for k, v := range newMediaType.Spec.Examples {
+						prevMediaType.Spec.Examples[k] = v
+					}
+				}
+				if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil {
+					oneOfSchema := spec.NewSchemaSpec()
+					oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema}
+					prevMediaType.Spec.Schema = oneOfSchema
+				} else {
+					prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema)
+				}
+			}
+		}
+	}
+
+	if code != "" {
+		o.Responses.Spec.Response[code] = res
+	} else {
+		o.Responses.Spec.Default = res
+	}
 }
 
 // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200.
 func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error {
 	for _, codeStr := range strings.Split(commentLine, ",") {
+		var description string
 		if strings.EqualFold(codeStr, defaultTag) {
-			_ = o.DefaultResponse()
-
-			continue
-		}
-
-		code, err := strconv.Atoi(codeStr)
-		if err != nil {
-			return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
+			codeStr = ""
+		} else {
+			code, err := strconv.Atoi(codeStr)
+			if err != nil {
+				return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
+			}
+			description = http.StatusText(code)
 		}
 
-		o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code)))
+		o.AddResponse(codeStr, newResponseWithDescription(description))
 	}
 
 	return nil
diff --git a/parserv3.go b/parserv3.go
index 6e25e7b..5027d1e 100644
--- a/parserv3.go
+++ b/parserv3.go
@@ -15,8 +15,8 @@ import (
 	"github.com/sv-tools/openapi/spec"
 )
 
-// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser.
-type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3
+// FieldParserFactoryV3 create FieldParser.
+type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3
 
 // FieldParserV3 parse struct field.
 type FieldParserV3 interface {
@@ -920,7 +920,7 @@ func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[strin
 		}
 	}
 
-	ps := p.fieldParserFactoryV3(p, field)
+	ps := p.fieldParserFactoryV3(p, file, field)
 
 	if ps.ShouldSkip() {
 		return nil, nil, nil


Test patch:
diff --git a/field_parser_v3_test.go b/field_parser_v3_test.go
index da544b9..8b0b80c 100644
--- a/field_parser_v3_test.go
+++ b/field_parser_v3_test.go
@@ -16,6 +16,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" example:"one"`,
 			}},
@@ -27,6 +28,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" example:""`,
 			}},
@@ -38,6 +40,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"float"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" example:"one"`,
 			}},
@@ -52,6 +55,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" format:"csv"`,
 			}},
@@ -65,6 +69,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 
 		got, err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" binding:"required"`,
 			}},
@@ -74,6 +79,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 
 		got, err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required"`,
 			}},
@@ -89,6 +95,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 			&Parser{
 				RequiredByDefault: true,
 			},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test"`,
 			}},
@@ -104,6 +111,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 			&Parser{
 				RequiredByDefault: true,
 			},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" binding:"optional"`,
 			}},
@@ -115,6 +123,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 			&Parser{
 				RequiredByDefault: true,
 			},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"optional"`,
 			}},
@@ -131,6 +140,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Extensions = map[string]interface{}{}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" extensions:"x-nullable,x-abc=def,!x-omitempty,x-example=[0, 9],x-example2={çãíœ, (bar=(abc, def)), [0,9]}"`,
 			}},
@@ -150,6 +160,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" enums:"a,b,c"`,
 			}},
@@ -161,6 +172,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"float"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" enums:"a,b,c"`,
 			}},
@@ -177,6 +189,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Enum = []interface{}{}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`,
 			}},
@@ -188,6 +201,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"int"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" enums:"0,1,2,3" x-enum-varnames:"Daily,Weekly,Monthly"`,
 			}},
@@ -204,6 +218,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Enum = []interface{}{}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`,
 			}},
@@ -220,6 +235,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" default:"pass"`,
 			}},
@@ -231,6 +247,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"float"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" default:"pass"`,
 			}},
@@ -245,6 +262,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maximum:"1"`,
 			}},
@@ -257,6 +275,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maximum:"one"`,
 			}},
@@ -267,6 +286,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"number"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maximum:"1"`,
 			}},
@@ -279,6 +299,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"number"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maximum:"one"`,
 			}},
@@ -289,6 +310,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"number"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" multipleOf:"1"`,
 			}},
@@ -301,6 +323,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"number"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" multipleOf:"one"`,
 			}},
@@ -311,6 +334,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" minimum:"1"`,
 			}},
@@ -323,6 +347,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" minimum:"one"`,
 			}},
@@ -337,6 +362,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maxLength:"1"`,
 			}},
@@ -349,6 +375,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" maxLength:"one"`,
 			}},
@@ -359,6 +386,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" minLength:"1"`,
 			}},
@@ -371,6 +399,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" minLength:"one"`,
 			}},
@@ -385,6 +414,7 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" readonly:"true"`,
 			}},
@@ -392,6 +422,24 @@ func TestDefaultFieldParserV3(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, true, schema.Spec.ReadOnly)
 	})
+
+	t.Run("OneOf tag", func(t *testing.T) {
+		t.Parallel()
+
+		schema := spec.NewSchemaSpec()
+		schema.Spec.Type = []string{ANY}
+		err := newTagBaseFieldParserV3(
+			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
+			&ast.Field{Tag: &ast.BasicLit{
+				Value: `json:"test" oneOf:"string,float64"`,
+			}},
+		).ComplementSchema(schema)
+		assert.NoError(t, err)
+		assert.Len(t, schema.Spec.OneOf, 2)
+		assert.Equal(t, spec.NewSingleOrArray("string"), schema.Spec.OneOf[0].Spec.Type)
+		assert.Equal(t, spec.NewSingleOrArray("number"), schema.Spec.OneOf[1].Spec.Type)
+	})
 }
 
 func TestValidTagsV3(t *testing.T) {
@@ -402,6 +450,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=10,min=1"`,
 			}},
@@ -416,6 +465,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=10,gte=1"`,
 			}},
@@ -428,6 +478,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=10,min=1"`,
 			}},
@@ -446,6 +497,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=10,min=1"`,
 			}},
@@ -457,6 +509,7 @@ func TestValidTagsV3(t *testing.T) {
 		// wrong validate tag will be ignored.
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=ten,min=1"`,
 			}},
@@ -473,6 +526,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof='red book' 'green book'"`,
 			}},
@@ -484,6 +538,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof=1 2 3"`,
 			}},
@@ -499,6 +554,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof=red green yellow"`,
 			}},
@@ -510,6 +566,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof='red green' blue 'c0x2Cc' 'd0x7Cd'"`,
 			}},
@@ -521,6 +578,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof='c0x9Ab' book"`,
 			}},
@@ -532,6 +590,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" binding:"oneof=foo bar" validate:"required,oneof=foo bar" enums:"a,b,c"`,
 			}},
@@ -543,6 +602,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" binding:"oneof=aa bb" validate:"required,oneof=foo bar"`,
 			}},
@@ -560,6 +620,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,unique"`,
 			}},
@@ -577,6 +638,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,unique,max=10,min=1,oneof=a0x2Cc 'c0x7Cd book',omitempty,dive,max=1"`,
 			}},
@@ -597,6 +659,7 @@ func TestValidTagsV3(t *testing.T) {
 
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof=,max=10=90,min=1"`,
 			}},
@@ -612,6 +675,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Items.Schema.Spec.Type = []string{"string"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,max=10,min=one"`,
 			}},
@@ -624,6 +688,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = []string{"integer"}
 		err = newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" validate:"required,oneof=one two"`,
 			}},
@@ -641,6 +706,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Items.Schema.Spec.Type = []string{"string"}
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`,
 			}},
@@ -656,6 +722,7 @@ func TestValidTagsV3(t *testing.T) {
 		schema.Spec.Type = typeString
 		err := newTagBaseFieldParserV3(
 			&Parser{},
+			&ast.File{Name: &ast.Ident{Name: "test"}},
 			&ast.Field{Tag: &ast.BasicLit{
 				Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`,
 			}},
diff --git a/parserv3_test.go b/parserv3_test.go
index 57e3afa..c1b561e 100644
--- a/parserv3_test.go
+++ b/parserv3_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/sv-tools/openapi/spec"
 )
 
 func TestOverridesGetTypeSchemaV3(t *testing.T) {
@@ -362,7 +363,6 @@ func TestParseSimpleApiV3(t *testing.T) {
 	assert.NoError(t, err)
 
 	paths := p.openAPI.Paths.Spec.Paths
-	assert.Equal(t, 15, len(paths))
 
 	path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec
 	assert.Equal(t, "get string by ID", path.Description)
@@ -376,6 +376,98 @@ func TestParseSimpleApiV3(t *testing.T) {
 	assert.NotNil(t, path)
 	assert.NotNil(t, path.RequestBody)
 	//TODO add asserts
+
+	t.Run("Test parse struct oneOf", func(t *testing.T) {
+		t.Parallel()
+
+		assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.OneOfTest")
+		schema := p.openAPI.Components.Spec.Schemas["web.OneOfTest"].Spec
+		expected := `{
+    "properties": {
+        "big_int": {
+            "oneOf": [
+                {
+                    "type": "string"
+                },
+                {
+                    "type": "integer"
+                }
+            ]
+        },
+        "pet_detail": {
+            "oneOf": [
+                {
+                    "$ref": "#/components/schemas/web.Cat"
+                },
+                {
+                    "$ref": "#/components/schemas/web.Dog"
+                }
+            ]
+        }
+    },
+    "type": "object"
+}`
+		out, err := json.MarshalIndent(schema, "", "    ")
+		assert.NoError(t, err)
+		assert.Equal(t, expected, string(out))
+
+		assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Cat")
+		schema = p.openAPI.Components.Spec.Schemas["web.Cat"].Spec
+		expected = `{
+    "properties": {
+        "age": {
+            "type": "integer"
+        },
+        "hunts": {
+            "type": "boolean"
+        }
+    },
+    "type": "object"
+}`
+		out, err = json.MarshalIndent(schema, "", "    ")
+		assert.NoError(t, err)
+		assert.Equal(t, expected, string(out))
+
+		assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Dog")
+		schema = p.openAPI.Components.Spec.Schemas["web.Dog"].Spec
+		expected = `{
+    "properties": {
+        "bark": {
+            "type": "boolean"
+        },
+        "breed": {
+            "enum": [
+                "Dingo",
+                "Husky",
+                "Retriever",
+                "Shepherd"
+            ],
+            "type": "string"
+        }
+    },
+    "type": "object"
+}`
+		out, err = json.MarshalIndent(schema, "", "    ")
+		assert.NoError(t, err)
+		assert.Equal(t, expected, string(out))
+	})
+
+	t.Run("Test parse response oneOf", func(t *testing.T) {
+		t.Parallel()
+
+		assert.Contains(t, paths, "/pets/{id}")
+		path := paths["/pets/{id}"]
+		assert.Contains(t, path.Spec.Spec.Get.Spec.Responses.Spec.Response, "200")
+		response = path.Spec.Spec.Get.Spec.Responses.Spec.Response["200"]
+		assert.Equal(t, "Return Cat or Dog", response.Spec.Spec.Description)
+		mediaType := response.Spec.Spec.Content["application/json"]
+		rootSchema := mediaType.Spec.Schema.Spec
+		assert.Equal(t, []*spec.RefOrSpec[spec.Schema]{
+			{Ref: &spec.Ref{Ref: "#/components/schemas/web.Cat"}},
+			{Ref: &spec.Ref{Ref: "#/components/schemas/web.Dog"}},
+		}, rootSchema.OneOf)
+
+	})
 }
 
 func TestParserParseServers(t *testing.T) {
diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go
index 91433b4..39ca8ba 100644
--- a/testdata/v3/simple/api/api.go
+++ b/testdata/v3/simple/api/api.go
@@ -144,3 +144,19 @@ func GetPet6FunctionScopedResponse() {
 func FormData() {
 
 }
+
+// @Success 200 {object} web.OneOfTest
+// @Router /GetOneOfTypes [get]
+func GetOneOfTypes() {
+
+}
+
+// @Summary Get pet by ID
+// @Param id path string true "ID"
+// @Success 200 {object} web.Cat
+// @Success 200 {object} web.Dog
+// @Success 200 "Return Cat or Dog"
+// @Router /pets/{id} [get]
+func GetPetByID() {
+
+}
diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go
index 3afcdd0..a8e03ac 100644
--- a/testdata/v3/simple/web/handler.go
+++ b/testdata/v3/simple/web/handler.go
@@ -112,3 +112,18 @@ type Request struct {
 	Password     string `json:"password"`
 	RefreshToken string `json:"refresh_token"`
 }
+
+type OneOfTest struct {
+	BigInt    any `json:"big_int" oneOf:"string,int64"`
+	PetDetail any `json:"pet_detail" oneOf:"Cat,Dog"`
+}
+
+type Dog struct {
+	Bark  bool   `json:"bark"`
+	Breed string `json:"breed" enums:"Dingo,Husky,Retriever,Shepherd"`
+}
+
+type Cat struct {
+	Hunts bool `json:"hunts"`
+	Age   int  `json:"age"`
+}