Repository: swaggo/swag
Pull request #1878 (2024-08-30 04:14:23)

feat: Add x-enum-descriptions to generated Swagger documentation for Enum
This pull request introduces the `x-enum-descriptions` extension to the generated Swagger documentation when working with enums. The new extension provides a list of descriptions for enum values, aligned with the x-enum-varnames.

## Details of the changes:

A new constant enumDescriptionsExtension has been added to support the x-enum-descriptions extension.
The ParseDefinition function has been updated to create a x-enum-descriptions list based on the order of x-enum-varnames and corresponding enum comments.

The `x-enum-comments` and `x-enum-varnames` are now matched in order, and their descriptions are output in the `x-enum-descriptionfs` array.
Test cases have been updated to reflect the addition of x-enum-descriptions, demonstrating the new output in the expected JSON.

## Why this change is necessary:

The addition of x-enum-descriptions enhances the clarity and usability of Swagger documentation by ensuring that descriptions for enum values are explicitly defined and correctly ordered. This can be particularly useful for clients and developers who rely on clear enum definitions and descriptions in API documentation.

## Testing:

Updated relevant test cases in testdata/enums/expected.json to include x-enum-descriptions.
Verified that the descriptions are correctly aligned with x-enum-varnames and appear in the expected order.
Impact:

This change does not affect existing functionality but adds a new optional extension to the generated documentation.
No breaking changes are introduced, and the update is backward compatible.
Please review the changes and let me know if there are any adjustments or additional tests needed. Your feedback is appreciated!


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9f92be3..995e235 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,9 +2,9 @@ name: build
 
 on:
   push:
-    branches: [ master ]
+    branches: [ master, v2 ]
   pull_request:
-    branches: [ master ]
+    branches: [ master, v2 ]
 
 jobs:
   test:
diff --git a/README.md b/README.md
index b453c41..010c44a 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,8 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie
 	- [User defined structure with an array type](#user-defined-structure-with-an-array-type)
 	- [Function scoped struct declaration](#function-scoped-struct-declaration)
 	- [Model composition in response](#model-composition-in-response)
-	- [Add a headers in response](#add-a-headers-in-response)
+        - [Add request headers](#add-request-headers)
+	- [Add response headers](#add-response-headers)
 	- [Use multiple path params](#use-multiple-path-params)
 	- [Example value of struct](#example-value-of-struct)
 	- [SchemaExample of body](#schemaexample-of-body)
@@ -103,6 +104,7 @@ OPTIONS:
    --outputTypes value, --ot value        Output types of generated files (docs.go, swagger.json, swagger.yaml) like go,json,yaml (default: "go,json,yaml")
    --parseVendor                          Parse go files in 'vendor' folder, disabled by default (default: false)
    --parseDependency, --pd                Parse go files inside dependency folder, disabled by default (default: false)
+   --parseDependencyLevel, --pdl          Enhancement of '--parseDependency', parse go files inside dependency folder, 0 disabled, 1 only parse models, 2 only parse operations, 3 parse all (default: 0)
    --markdownFiles value, --md value      Parse folder containing markdown files to use as description, disabled by default
    --codeExampleFiles value, --cef value  Parse folder containing code example files to use for the x-codeSamples extension, disabled by default
    --parseInternal                        Parse go files in internal packages, disabled by default (default: false)
@@ -418,6 +420,7 @@ When a short string in your documentation is insufficient, or you need images, c
 | description.markdown  | A short description of the application. Parsed from the api.md file. This is an alternative to @description    |// @description.markdown No value needed, this parses the description from api.md         																 |
 | tag.name    | Name of a tag.| // @tag.name This is the name of the tag                     |
 | tag.description.markdown   | Description of the tag this is an alternative to tag.description. The description will be read from a file named like tagname.md  | // @tag.description.markdown         |
+| tag.x-name  | The extension key, must be start by x- and take only string value | // @x-example-key value |
 
 
 ## API Operation
@@ -646,7 +649,14 @@ type DeepObject struct { //in `proto` package
 }
 @success 200 {object} jsonresult.JSONResult{data1=proto.Order{data=proto.DeepObject},data2=[]proto.Order{data=[]proto.DeepObject}} "desc"
 ```
-### Add a headers in response
+### Add response request
+
+```go
+// @Param        X-MyHeader	  header    string    true   	"MyHeader must be set for valid response"
+// @Param        X-API-VERSION    header    string    true   	"API version eg.: 1.0"
+```
+
+### Add response headers
 
 ```go
 // @Success      200              {string}  string    "ok"
@@ -947,6 +957,17 @@ swag init -g http/api.go -td "[[,]]"
 ```
 The new delimiter is a string with the format "`<left delimiter>`,`<right delimiter>`".
 
+### Parse Internal and Dependency Packages
+
+If the struct is defined in a dependency package, use `--parseDependency`.
+
+If the struct is defined in your main project, use `--parseInternal`.
+
+if you want to include both internal and from dependencies use both flags 
+```
+swag init --parseDependency --parseInternal
+```
+
 ## About the Project
 This project was inspired by [yvasiyarov/swagger](https://github.com/yvasiyarov/swagger) but we simplified the usage and added support a variety of [web frameworks](#supported-web-frameworks). Gopher image source is [tenntenn/gopher-stickers](https://github.com/tenntenn/gopher-stickers). It has licenses [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en).
 ## Contributors
diff --git a/README_zh-CN.md b/README_zh-CN.md
index 2b3d9d2..24a219c 100644
--- a/README_zh-CN.md
+++ b/README_zh-CN.md
@@ -90,6 +90,7 @@ OPTIONS:
    --output value, -o value               文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs")
    --parseVendor                          是否解析vendor目录里的go源文件，默认不
    --parseDependency                      是否解析依赖目录中的go源文件，默认不
+   --parseDependencyLevel, --pdl          对'--parseDependency'参数进行增强, 是否解析依赖目录中的go源文件, 0 不解析, 1 只解析对象模型, 2 只解析API, 3 对象模型和API都解析 (default: 0)
    --markdownFiles value, --md value      指定API的描述信息所使用的markdown文件所在的目录
    --generatedTime                        是否输出时间到输出文件docs.go的顶部，默认是
    --codeExampleFiles value, --cef value  解析包含用于 x-codeSamples 扩展的代码示例文件的文件夹，默认禁用
diff --git a/enums.go b/enums.go
index 38658f2..300787b 100644
--- a/enums.go
+++ b/enums.go
@@ -1,8 +1,9 @@
 package swag
 
 const (
-	enumVarNamesExtension = "x-enum-varnames"
-	enumCommentsExtension = "x-enum-comments"
+	enumVarNamesExtension     = "x-enum-varnames"
+	enumCommentsExtension     = "x-enum-comments"
+	enumDescriptionsExtension = "x-enum-descriptions"
 )
 
 // EnumValue a model to record an enum consts variable
diff --git a/field_parser.go b/field_parser.go
index f2bef6f..de8945d 100644
--- a/field_parser.go
+++ b/field_parser.go
@@ -65,35 +65,38 @@ func (ps *tagBaseFieldParser) ShouldSkip() bool {
 	return false
 }
 
-func (ps *tagBaseFieldParser) FieldName() (string, error) {
-	var name string
+func (ps *tagBaseFieldParser) FieldNames() ([]string, error) {
+	if len(ps.field.Names) <= 1 {
+		// if embedded but with a json/form name ??
+		if ps.field.Tag != nil {
+			// json:"tag,hoge"
+			name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0])
+			if name != "" {
+				return []string{name}, nil
+			}
 
-	if ps.field.Tag != nil {
-		// json:"tag,hoge"
-		name = strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0])
-		if name != "" {
-			return name, nil
+			// use "form" tag over json tag
+			name = ps.FormName()
+			if name != "" {
+				return []string{name}, nil
+			}
 		}
-
-		// use "form" tag over json tag
-		name = ps.FormName()
-		if name != "" {
-			return name, nil
+		if len(ps.field.Names) == 0 {
+			return nil, nil
 		}
 	}
-
-	if ps.field.Names == nil {
-		return "", nil
-	}
-
-	switch ps.p.PropNamingStrategy {
-	case SnakeCase:
-		return toSnakeCase(ps.field.Names[0].Name), nil
-	case PascalCase:
-		return ps.field.Names[0].Name, nil
-	default:
-		return toLowerCamelCase(ps.field.Names[0].Name), nil
+	var names = make([]string, 0, len(ps.field.Names))
+	for _, name := range ps.field.Names {
+		switch ps.p.PropNamingStrategy {
+		case SnakeCase:
+			names = append(names, toSnakeCase(name.Name))
+		case PascalCase:
+			names = append(names, name.Name)
+		default:
+			names = append(names, toLowerCamelCase(name.Name))
+		}
 	}
+	return names, nil
 }
 
 func (ps *tagBaseFieldParser) firstTagValue(tag string) string {
diff --git a/operation.go b/operation.go
index ac79e31..ba3a05b 100644
--- a/operation.go
+++ b/operation.go
@@ -281,7 +281,7 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F
 
 	requiredText := strings.ToLower(matches[4])
 	required := requiredText == "true" || requiredText == requiredLabel
-	description := matches[5]
+	description := strings.Join(strings.Split(matches[5], "\\n"), "\n")
 
 	param := createParameter(paramType, description, name, objectType, refType, required, enums, operation.parser.collectionFormatInQuery)
 
@@ -707,7 +707,7 @@ func parseMimeTypeList(mimeTypeList string, typeList *[]string, format string) e
 	return nil
 }
 
-var routerPattern = regexp.MustCompile(`^(/[\w./\-{}+:$]*)[[:blank:]]+\[(\w+)]`)
+var routerPattern = regexp.MustCompile(`^(/[\w./\-{}\(\)+:$]*)[[:blank:]]+\[(\w+)]`)
 
 // ParseRouterComment parses comment for given `router` comment string.
 func (operation *Operation) ParseRouterComment(commentLine string, deprecated bool) error {
diff --git a/packages.go b/packages.go
index 479b263..2032414 100644
--- a/packages.go
+++ b/packages.go
@@ -252,6 +252,8 @@ func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *as
 										pkgDefs.uniqueDefinitions[fullName] = nil
 										anotherTypeDef.NotUnique = true
 										pkgDefs.uniqueDefinitions[anotherTypeDef.TypeName()] = anotherTypeDef
+										anotherTypeDef.SetSchemaName()
+
 										typeSpecDef.NotUnique = true
 										fullName = typeSpecDef.TypeName()
 										pkgDefs.uniqueDefinitions[fullName] = typeSpecDef
@@ -261,6 +263,8 @@ func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *as
 									functionScopedTypes[typeSpec.Name.Name] = typeSpecDef
 								}
 
+								typeSpecDef.SetSchemaName()
+
 								if pkgDefs.packages[typeSpecDef.PkgPath] == nil {
 									pkgDefs.packages[typeSpecDef.PkgPath] = NewPackageDefinitions(astFile.Name.Name, typeSpecDef.PkgPath).AddTypeSpec(fullName, typeSpecDef)
 								} else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName]; !ok {
@@ -378,7 +382,7 @@ func (pkgDefs *PackagesDefinitions) collectConstEnums(parsedSchemas map[*TypeSpe
 				continue
 			}
 
-			//delete it from parsed schemas, and will parse it again
+			// delete it from parsed schemas, and will parse it again
 			if _, ok = parsedSchemas[typeDef]; ok {
 				delete(parsedSchemas, typeDef)
 			}
@@ -494,7 +498,7 @@ func (pkgDefs *PackagesDefinitions) findPackagePathFromImports(pkg string, file
 				}
 				break
 			} else if imp.Name.Name == "_" && len(pkg) > 0 {
-				//for unused types
+				// for unused types
 				pd, ok := pkgDefs.packages[path]
 				if ok {
 					if pd.Name == pkg {
@@ -594,8 +598,12 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File
 		return pkgDefs.parametrizeGenericType(file, typeDef, typeName)
 	}
 
-	//in case that comment //@name renamed the type with a name without a dot
-	for _, v := range pkgDefs.uniqueDefinitions {
+	// in case that comment //@name renamed the type with a name without a dot
+	for k, v := range pkgDefs.uniqueDefinitions {
+		if v == nil {
+			pkgDefs.debug.Printf("%s TypeSpecDef is nil", k)
+			continue
+		}
 		if v.SchemaName == typeName {
 			return v
 		}
diff --git a/parser.go b/parser.go
index b994057..9db79fa 100644
--- a/parser.go
+++ b/parser.go
@@ -190,7 +190,7 @@ type FieldParserFactory func(ps *Parser, field *ast.Field) FieldParser
 // FieldParser parse struct field.
 type FieldParser interface {
 	ShouldSkip() bool
-	FieldName() (string, error)
+	FieldNames() ([]string, error)
 	FormName() string
 	HeaderName() string
 	PathName() string
@@ -676,6 +676,21 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error {
 
 					parser.swagger.Extensions[attribute[1:]] = valueJSON
 				}
+			} else if strings.HasPrefix(attribute, "@tag.x-") {
+				extensionName := attribute[5:]
+
+				if len(value) == 0 {
+					return fmt.Errorf("annotation %s need a value", attribute)
+				}
+
+				if tag.Extensions == nil {
+					tag.Extensions = make(map[string]interface{})
+				}
+
+				// tag.Extensions.Add(extensionName, value) works wrong (transforms extensionName to lower case)
+				// needed to save case for ReDoc
+				// https://redocly.com/docs/api-reference-docs/specification-extensions/x-display-name/
+				tag.Extensions[extensionName] = value
 			}
 		}
 
@@ -1294,17 +1309,22 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error)
 	}
 
 	if definition.Description == "" {
-		fillDefinitionDescription(definition, typeSpecDef.File, typeSpecDef)
+		err = parser.fillDefinitionDescription(definition, typeSpecDef.File, typeSpecDef)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	if len(typeSpecDef.Enums) > 0 {
 		var varnames []string
 		var enumComments = make(map[string]string)
+		var enumDescriptions = make([]string, 0, len(typeSpecDef.Enums))
 		for _, value := range typeSpecDef.Enums {
 			definition.Enum = append(definition.Enum, value.Value)
 			varnames = append(varnames, value.key)
 			if len(value.Comment) > 0 {
 				enumComments[value.key] = value.Comment
+				enumDescriptions = append(enumDescriptions, value.Comment)
 			}
 		}
 		if definition.Extensions == nil {
@@ -1313,6 +1333,7 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error)
 		definition.Extensions[enumVarNamesExtension] = varnames
 		if len(enumComments) > 0 {
 			definition.Extensions[enumCommentsExtension] = enumComments
+			definition.Extensions[enumDescriptionsExtension] = enumDescriptions
 		}
 	}
 
@@ -1344,7 +1365,7 @@ func fullTypeName(parts ...string) string {
 
 // fillDefinitionDescription additionally fills fields in definition (spec.Schema)
 // TODO: If .go file contains many types, it may work for a long time
-func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) {
+func (parser *Parser) fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) (err error) {
 	if file == nil {
 		return
 	}
@@ -1359,16 +1380,23 @@ func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpec
 			if !ok || typeSpec != typeSpecDef.TypeSpec {
 				continue
 			}
-
-			definition.Description =
-				extractDeclarationDescription(typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc)
+			var typeName string
+			if typeSpec.Name != nil {
+				typeName = typeSpec.Name.Name
+			}
+			definition.Description, err =
+				parser.extractDeclarationDescription(typeName, typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc)
+			if err != nil {
+				return
+			}
 		}
 	}
+	return nil
 }
 
 // extractDeclarationDescription gets first description
 // from attribute descriptionAttr in commentGroups (ast.CommentGroup)
-func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string {
+func (parser *Parser) extractDeclarationDescription(typeName string, commentGroups ...*ast.CommentGroup) (string, error) {
 	var description string
 
 	for _, commentGroup := range commentGroups {
@@ -1383,9 +1411,23 @@ func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string {
 			if len(commentText) == 0 {
 				continue
 			}
-			attribute := FieldsByAnySpace(commentText, 2)[0]
+			fields := FieldsByAnySpace(commentText, 2)
+			attribute := fields[0]
 
-			if strings.ToLower(attribute) != descriptionAttr {
+			if attr := strings.ToLower(attribute); attr == descriptionMarkdownAttr {
+				if len(fields) > 1 {
+					typeName = fields[1]
+				}
+				if typeName == "" {
+					continue
+				}
+				desc, err := getMarkdownForTag(typeName, parser.markdownFileDir)
+				if err != nil {
+					return "", err
+				}
+				// if found markdown description, we will only use the markdown file content
+				return string(desc), nil
+			} else if attr != descriptionAttr {
 				if !isHandlingDescription {
 					continue
 				}
@@ -1398,7 +1440,7 @@ func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string {
 		}
 	}
 
-	return strings.TrimLeft(description, " ")
+	return strings.TrimLeft(description, " "), nil
 }
 
 // parseTypeExpr parses given type expression that corresponds to the type under
@@ -1503,20 +1545,20 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st
 		return nil, nil, nil
 	}
 
-	fieldName, err := ps.FieldName()
+	fieldNames, err := ps.FieldNames()
 	if err != nil {
 		return nil, nil, err
 	}
 
-	if fieldName == "" {
+	if len(fieldNames) == 0 {
 		typeName, err := getFieldType(file, field.Type, nil)
 		if err != nil {
-			return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+			return nil, nil, err
 		}
 
 		schema, err := parser.getTypeSchema(typeName, file, false)
 		if err != nil {
-			return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+			return nil, nil, err
 		}
 
 		if len(schema.Type) > 0 && schema.Type[0] == OBJECT {
@@ -1538,7 +1580,7 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st
 
 	schema, err := ps.CustomSchema()
 	if err != nil {
-		return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+		return nil, nil, fmt.Errorf("%v: %w", fieldNames, err)
 	}
 
 	if schema == nil {
@@ -1552,24 +1594,24 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st
 		}
 
 		if err != nil {
-			return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+			return nil, nil, fmt.Errorf("%v: %w", fieldNames, err)
 		}
 	}
 
 	err = ps.ComplementSchema(schema)
 	if err != nil {
-		return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+		return nil, nil, fmt.Errorf("%v: %w", fieldNames, err)
 	}
 
 	var tagRequired []string
 
 	required, err := ps.IsRequired()
 	if err != nil {
-		return nil, nil, fmt.Errorf("%s: %w", fieldName, err)
+		return nil, nil, fmt.Errorf("%v: %w", fieldNames, err)
 	}
 
 	if required {
-		tagRequired = append(tagRequired, fieldName)
+		tagRequired = append(tagRequired, fieldNames...)
 	}
 
 	if schema.Extensions == nil {
@@ -1584,8 +1626,11 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st
 	if pathName := ps.PathName(); len(pathName) > 0 {
 		schema.Extensions["path"] = pathName
 	}
-
-	return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil
+	fields := make(map[string]spec.Schema)
+	for _, name := range fieldNames {
+		fields[name] = *schema
+	}
+	return fields, tagRequired, nil
 }
 
 func getFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) {
diff --git a/version.go b/version.go
index ff2810e..cf52320 100644
--- a/version.go
+++ b/version.go
@@ -1,4 +1,4 @@
 package swag
 
 // Version of swag.
-const Version = "v1.16.3"
+const Version = "v1.16.4"


Test patch:
diff --git a/enums_test.go b/enums_test.go
index e345637..d6645c7 100644
--- a/enums_test.go
+++ b/enums_test.go
@@ -2,6 +2,7 @@ package swag
 
 import (
 	"encoding/json"
+	"math/bits"
 	"os"
 	"path/filepath"
 	"testing"
@@ -21,7 +22,7 @@ func TestParseGlobalEnums(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, string(expected), string(b))
 	constsPath := "github.com/swaggo/swag/testdata/enums/consts"
-	assert.Equal(t, 64, p.packages.packages[constsPath].ConstTable["uintSize"].Value)
+	assert.Equal(t, bits.UintSize, p.packages.packages[constsPath].ConstTable["uintSize"].Value)
 	assert.Equal(t, int32(62), p.packages.packages[constsPath].ConstTable["maxBase"].Value)
 	assert.Equal(t, 8, p.packages.packages[constsPath].ConstTable["shlByLen"].Value)
 	assert.Equal(t, 255, p.packages.packages[constsPath].ConstTable["hexnum"].Value)
diff --git a/field_parser_test.go b/field_parser_test.go
index 0d8723f..28f8cdf 100644
--- a/field_parser_test.go
+++ b/field_parser_test.go
@@ -676,9 +676,11 @@ func TestValidTags(t *testing.T) {
 		schema.Type = []string{"integer"}
 		err = newTagBaseFieldParser(
 			&Parser{},
-			&ast.Field{Tag: &ast.BasicLit{
-				Value: `json:"test" validate:"required,oneof=one two"`,
-			}},
+			&ast.Field{
+				Names: []*ast.Ident{{Name: "Test"}},
+				Tag: &ast.BasicLit{
+					Value: `json:"test" validate:"required,oneof=one two"`,
+				}},
 		).ComplementSchema(&schema)
 		assert.NoError(t, err)
 		assert.Empty(t, schema.Enum)
@@ -687,22 +689,41 @@ func TestValidTags(t *testing.T) {
 	t.Run("Form Filed Name", func(t *testing.T) {
 		t.Parallel()
 
-		filedname, err := newTagBaseFieldParser(
+		filednames, err := newTagBaseFieldParser(
 			&Parser{},
-			&ast.Field{Tag: &ast.BasicLit{
-				Value: `form:"test[]"`,
-			}},
-		).FieldName()
+			&ast.Field{
+				Names: []*ast.Ident{{Name: "Test"}},
+				Tag: &ast.BasicLit{
+					Value: `form:"test[]"`,
+				}},
+		).FieldNames()
 		assert.NoError(t, err)
-		assert.Equal(t, "test", filedname)
+		assert.Equal(t, "test", filednames[0])
 
-		filedname, err = newTagBaseFieldParser(
+		filednames, err = newTagBaseFieldParser(
 			&Parser{},
-			&ast.Field{Tag: &ast.BasicLit{
-				Value: `form:"test"`,
-			}},
-		).FieldName()
+			&ast.Field{
+				Names: []*ast.Ident{{Name: "Test"}},
+				Tag: &ast.BasicLit{
+					Value: `form:"test"`,
+				}},
+		).FieldNames()
+		assert.NoError(t, err)
+		assert.Equal(t, "test", filednames[0])
+	})
+
+	t.Run("Two Names", func(t *testing.T) {
+		t.Parallel()
+
+		fieldnames, err := newTagBaseFieldParser(
+			&Parser{},
+			&ast.Field{
+				Names: []*ast.Ident{{Name: "X"}, {Name: "Y"}},
+			},
+		).FieldNames()
 		assert.NoError(t, err)
-		assert.Equal(t, "test", filedname)
+		assert.Equal(t, 2, len(fieldnames))
+		assert.Equal(t, "x", fieldnames[0])
+		assert.Equal(t, "y", fieldnames[1])
 	})
 }
diff --git a/generics_test.go b/generics_test.go
index 033bcf5..fecaa4d 100644
--- a/generics_test.go
+++ b/generics_test.go
@@ -134,6 +134,22 @@ func TestParseGenericsPackageAlias(t *testing.T) {
 	assert.Equal(t, string(expected), string(b))
 }
 
+func TestParseGenericsFunctionScoped(t *testing.T) {
+	t.Parallel()
+
+	searchDir := "testdata/generics_function_scoped"
+	expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json"))
+	assert.NoError(t, err)
+
+	p := New()
+	err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
+	assert.NoError(t, err)
+	b, err := json.MarshalIndent(p.swagger, "", "    ")
+
+	assert.NoError(t, err)
+	assert.Equal(t, string(expected), string(b))
+}
+
 func TestParametrizeStruct(t *testing.T) {
 	pd := PackagesDefinitions{
 		packages:          make(map[string]*PackageDefinitions),
diff --git a/operation_test.go b/operation_test.go
index 50e4ac3..e6bd13e 100644
--- a/operation_test.go
+++ b/operation_test.go
@@ -175,6 +175,18 @@ func TestParseRouterCommentWithDollarSign(t *testing.T) {
 	assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod)
 }
 
+func TestParseRouterCommentWithParens(t *testing.T) {
+	t.Parallel()
+
+	comment := `/@Router /customer({id}) [get]`
+	operation := NewOperation(nil)
+	err := operation.ParseComment(comment, nil)
+	assert.NoError(t, err)
+	assert.Len(t, operation.RouterProperties, 1)
+	assert.Equal(t, "/customer({id})", operation.RouterProperties[0].Path)
+	assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod)
+}
+
 func TestParseRouterCommentNoDollarSignAtPathStartErr(t *testing.T) {
 	t.Parallel()
 
@@ -1322,6 +1334,27 @@ func TestParseParamCommentByID(t *testing.T) {
 	assert.Equal(t, expected, string(b))
 }
 
+func TestParseParamCommentWithMultilineDescriptions(t *testing.T) {
+	t.Parallel()
+
+	comment := `@Param some_id query int true "First line\nSecond line\nThird line"`
+	operation := NewOperation(nil)
+	err := operation.ParseComment(comment, nil)
+
+	assert.NoError(t, err)
+	b, _ := json.MarshalIndent(operation.Parameters, "", "    ")
+	expected := `[
+    {
+        "type": "integer",
+        "description": "First line\nSecond line\nThird line",
+        "name": "some_id",
+        "in": "query",
+        "required": true
+    }
+]`
+	assert.Equal(t, expected, string(b))
+}
+
 func TestParseParamCommentByQueryType(t *testing.T) {
 	t.Parallel()
 
diff --git a/parser_test.go b/parser_test.go
index 6b64aec..1f56036 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -576,7 +576,8 @@ func TestParser_ParseGeneralAPITagDocs(t *testing.T) {
 		"@tag.name test",
 		"@tag.description A test Tag",
 		"@tag.docs.url https://example.com",
-		"@tag.docs.description Best example documentation"})
+		"@tag.docs.description Best example documentation",
+		"@tag.x-displayName Test group"})
 	assert.NoError(t, err)
 
 	b, _ := json.MarshalIndent(parser.GetSwagger().Tags, "", "    ")
@@ -587,7 +588,8 @@ func TestParser_ParseGeneralAPITagDocs(t *testing.T) {
         "externalDocs": {
             "description": "Best example documentation",
             "url": "https://example.com"
-        }
+        },
+        "x-displayName": "Test group"
     }
 ]`
 	assert.Equal(t, expected, string(b))
diff --git a/testdata/enums/expected.json b/testdata/enums/expected.json
index 79b56f8..9217f61 100644
--- a/testdata/enums/expected.json
+++ b/testdata/enums/expected.json
@@ -102,6 +102,10 @@
                             "A": "AAA",
                             "B": "BBB"
                         },
+                        "x-enum-descriptions": [
+                            "AAA",
+                            "BBB"
+                        ],
                         "x-enum-varnames": [
                             "None",
                             "A",
@@ -128,6 +132,12 @@
                             "Mask3": "Mask3",
                             "Mask4": "Mask4"
                         },
+                        "x-enum-descriptions": [
+                            "Mask1",
+                            "Mask2",
+                            "Mask3",
+                            "Mask4"
+                        ],
                         "x-enum-varnames": [
                             "Mask1",
                             "Mask2",
@@ -155,6 +165,11 @@
                             "Student": "student",
                             "Teacher": "teacher"
                         },
+                        "x-enum-descriptions": [
+                            "teacher",
+                            "student",
+                            "Other"
+                        ],
                         "x-enum-varnames": [
                             "Teacher",
                             "Student",
@@ -224,6 +239,11 @@
                             "Student": "student",
                             "Teacher": "teacher"
                         },
+                        "x-enum-descriptions": [
+                            "teacher",
+                            "student",
+                            "Other"
+                        ],
                         "x-enum-varnames": [
                             "Teacher",
                             "Student",
@@ -256,6 +276,10 @@
                 "A": "AAA",
                 "B": "BBB"
             },
+            "x-enum-descriptions": [
+                "AAA",
+                "BBB"
+            ],
             "x-enum-varnames": [
                 "None",
                 "A",
@@ -280,6 +304,12 @@
                 "Mask3": "Mask3",
                 "Mask4": "Mask4"
             },
+            "x-enum-descriptions": [
+                "Mask1",
+                "Mask2",
+                "Mask3",
+                "Mask4"
+            ],
             "x-enum-varnames": [
                 "Mask1",
                 "Mask2",
@@ -317,6 +347,11 @@
                 "Student": "student",
                 "Teacher": "teacher"
             },
+            "x-enum-descriptions": [
+                "teacher",
+                "student",
+                "Other"
+            ],
             "x-enum-varnames": [
                 "Teacher",
                 "Student",
diff --git a/testdata/generics_function_scoped/api/api.go b/testdata/generics_function_scoped/api/api.go
new file mode 100644
index 0000000..904bce3
--- /dev/null
+++ b/testdata/generics_function_scoped/api/api.go
@@ -0,0 +1,75 @@
+package api
+
+import (
+	"net/http"
+
+	"github.com/swaggo/swag/testdata/generics_function_scoped/types"
+)
+
+// @Summary Generic Response
+// @Produce  json
+// @Success 200 {object} types.GenericResponse[api.GetGeneric.User]
+// @Success 201 {object} types.GenericResponse[api.GetGeneric.Post]
+// @Router / [get]
+func GetGeneric(w http.ResponseWriter, r *http.Request) {
+	type User struct {
+		Username int    `json:"username"`
+		Email    string `json:"email"`
+	}
+	type Post struct {
+		Slug  int    `json:"slug"`
+		Title string `json:"title"`
+	}
+
+	_ = types.GenericResponse[any]{}
+}
+
+// @Summary Generic Response With Custom Type Names
+// @Produce  json
+// @Success 200 {object} types.GenericResponse[api.GetGenericRenamed.User]
+// @Success 201 {object} types.GenericResponse[api.GetGenericRenamed.Post]
+// @Router /renamed [get]
+func GetGenericRenamed(w http.ResponseWriter, r *http.Request) {
+	type User struct {
+		Username int    `json:"username"`
+		Email    string `json:"email"`
+	} // @Name RenamedUserData
+	type Post struct {
+		Slug  int    `json:"slug"`
+		Title string `json:"title"`
+	} // @Name RenamedPostData
+
+	_ = types.GenericResponse[any]{}
+}
+
+// @Summary Multiple Generic Response
+// @Produce  json
+// @Success 200 {object} types.GenericMultiResponse[api.GetGenericMulti.MyStructA, api.GetGenericMulti.MyStructB]
+// @Success 201 {object} types.GenericMultiResponse[api.GetGenericMulti.MyStructB, api.GetGenericMulti.MyStructA]
+// @Router /multi [get]
+func GetGenericMulti(w http.ResponseWriter, r *http.Request) {
+	type MyStructA struct {
+		SomeFieldA string `json:"some_field_a"`
+	}
+	type MyStructB struct {
+		SomeFieldB string `json:"some_field_b"`
+	}
+
+	_ = types.GenericMultiResponse[any, any]{}
+}
+
+// @Summary Multiple Generic Response With Custom Type Names
+// @Produce  json
+// @Success 200 {object} types.GenericMultiResponse[api.GetGenericMultiRenamed.MyStructA, api.GetGenericMultiRenamed.MyStructB]
+// @Success 201 {object} types.GenericMultiResponse[api.GetGenericMultiRenamed.MyStructB, api.GetGenericMultiRenamed.MyStructA]
+// @Router /multi-renamed [get]
+func GetGenericMultiRenamed(w http.ResponseWriter, r *http.Request) {
+	type MyStructA struct {
+		SomeFieldA string `json:"some_field_a"`
+	} // @Name NameForMyStructA
+	type MyStructB struct {
+		SomeFieldB string `json:"some_field_b"`
+	} // @Name NameForMyStructB
+
+	_ = types.GenericMultiResponse[any, any]{}
+}
diff --git a/testdata/generics_function_scoped/expected.json b/testdata/generics_function_scoped/expected.json
new file mode 100644
index 0000000..3fb99fa
--- /dev/null
+++ b/testdata/generics_function_scoped/expected.json
@@ -0,0 +1,279 @@
+{
+    "swagger": "2.0",
+    "info": {
+        "description": "This is a sample server.",
+        "title": "Swagger Example API",
+        "contact": {},
+        "version": "1.0"
+    },
+    "host": "localhost:8080",
+    "basePath": "/api",
+    "paths": {
+        "/": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "Generic Response",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericResponse-api_GetGeneric_User"
+                        }
+                    },
+                    "201": {
+                        "description": "Created",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericResponse-api_GetGeneric_Post"
+                        }
+                    }
+                }
+            }
+        },
+        "/multi": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "Multiple Generic Response",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericMultiResponse-api_GetGenericMulti_MyStructA-api_GetGenericMulti_MyStructB"
+                        }
+                    },
+                    "201": {
+                        "description": "Created",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericMultiResponse-api_GetGenericMulti_MyStructB-api_GetGenericMulti_MyStructA"
+                        }
+                    }
+                }
+            }
+        },
+        "/multi-renamed": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "Multiple Generic Response With Custom Type Names",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericMultiResponse-NameForMyStructA-NameForMyStructB"
+                        }
+                    },
+                    "201": {
+                        "description": "Created",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericMultiResponse-NameForMyStructB-NameForMyStructA"
+                        }
+                    }
+                }
+            }
+        },
+        "/renamed": {
+            "get": {
+                "produces": [
+                    "application/json"
+                ],
+                "summary": "Generic Response With Custom Type Names",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericResponse-RenamedUserData"
+                        }
+                    },
+                    "201": {
+                        "description": "Created",
+                        "schema": {
+                            "$ref": "#/definitions/types.GenericResponse-RenamedPostData"
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "definitions": {
+        "NameForMyStructA": {
+            "type": "object",
+            "properties": {
+                "some_field_a": {
+                    "type": "string"
+                }
+            }
+        },
+        "NameForMyStructB": {
+            "type": "object",
+            "properties": {
+                "some_field_b": {
+                    "type": "string"
+                }
+            }
+        },
+        "RenamedPostData": {
+            "type": "object",
+            "properties": {
+                "slug": {
+                    "type": "integer"
+                },
+                "title": {
+                    "type": "string"
+                }
+            }
+        },
+        "RenamedUserData": {
+            "type": "object",
+            "properties": {
+                "email": {
+                    "type": "string"
+                },
+                "username": {
+                    "type": "integer"
+                }
+            }
+        },
+        "api.GetGeneric.Post": {
+            "type": "object",
+            "properties": {
+                "slug": {
+                    "type": "integer"
+                },
+                "title": {
+                    "type": "string"
+                }
+            }
+        },
+        "api.GetGeneric.User": {
+            "type": "object",
+            "properties": {
+                "email": {
+                    "type": "string"
+                },
+                "username": {
+                    "type": "integer"
+                }
+            }
+        },
+        "api.GetGenericMulti.MyStructA": {
+            "type": "object",
+            "properties": {
+                "some_field_a": {
+                    "type": "string"
+                }
+            }
+        },
+        "api.GetGenericMulti.MyStructB": {
+            "type": "object",
+            "properties": {
+                "some_field_b": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericMultiResponse-NameForMyStructA-NameForMyStructB": {
+            "type": "object",
+            "properties": {
+                "data_t": {
+                    "$ref": "#/definitions/NameForMyStructA"
+                },
+                "data_x": {
+                    "$ref": "#/definitions/NameForMyStructB"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericMultiResponse-NameForMyStructB-NameForMyStructA": {
+            "type": "object",
+            "properties": {
+                "data_t": {
+                    "$ref": "#/definitions/NameForMyStructB"
+                },
+                "data_x": {
+                    "$ref": "#/definitions/NameForMyStructA"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericMultiResponse-api_GetGenericMulti_MyStructA-api_GetGenericMulti_MyStructB": {
+            "type": "object",
+            "properties": {
+                "data_t": {
+                    "$ref": "#/definitions/api.GetGenericMulti.MyStructA"
+                },
+                "data_x": {
+                    "$ref": "#/definitions/api.GetGenericMulti.MyStructB"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericMultiResponse-api_GetGenericMulti_MyStructB-api_GetGenericMulti_MyStructA": {
+            "type": "object",
+            "properties": {
+                "data_t": {
+                    "$ref": "#/definitions/api.GetGenericMulti.MyStructB"
+                },
+                "data_x": {
+                    "$ref": "#/definitions/api.GetGenericMulti.MyStructA"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericResponse-RenamedPostData": {
+            "type": "object",
+            "properties": {
+                "data": {
+                    "$ref": "#/definitions/RenamedPostData"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericResponse-RenamedUserData": {
+            "type": "object",
+            "properties": {
+                "data": {
+                    "$ref": "#/definitions/RenamedUserData"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericResponse-api_GetGeneric_Post": {
+            "type": "object",
+            "properties": {
+                "data": {
+                    "$ref": "#/definitions/api.GetGeneric.Post"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.GenericResponse-api_GetGeneric_User": {
+            "type": "object",
+            "properties": {
+                "data": {
+                    "$ref": "#/definitions/api.GetGeneric.User"
+                },
+                "status": {
+                    "type": "string"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/testdata/generics_function_scoped/main.go b/testdata/generics_function_scoped/main.go
new file mode 100644
index 0000000..7dd44c3
--- /dev/null
+++ b/testdata/generics_function_scoped/main.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"net/http"
+
+	"github.com/swaggo/swag/testdata/generics_function_scoped/api"
+)
+
+// @title Swagger Example API
+// @version 1.0
+// @description This is a sample server.
+// @host localhost:8080
+// @basePath /api
+func main() {
+	http.HandleFunc("/", api.GetGeneric)
+	http.HandleFunc("/renamed", api.GetGenericRenamed)
+	http.HandleFunc("/multi", api.GetGenericMulti)
+	http.HandleFunc("/multi-renamed", api.GetGenericMulti)
+	http.ListenAndServe(":8080", nil)
+}
diff --git a/testdata/generics_function_scoped/types/response.go b/testdata/generics_function_scoped/types/response.go
new file mode 100644
index 0000000..d12e532
--- /dev/null
+++ b/testdata/generics_function_scoped/types/response.go
@@ -0,0 +1,12 @@
+package types
+
+type GenericResponse[T any] struct {
+	Status string `json:"status"`
+	Data   T      `json:"data"`
+}
+
+type GenericMultiResponse[T any, X any] struct {
+	Status string `json:"status"`
+	DataT  T      `json:"data_t"`
+	DataX  X      `json:"data_x"`
+}