Repository: go-swagger/go-swagger
Pull request #3122 (2024-07-01 09:32:28)

fix: duplicated tags when using `x-go-custom-tag` extension
Go-swagger would generate duplicated tags while using the `x-go-custom-tag` extension.
for example:
```
    properties:
      tags:
        items:
          type: string
        type: array
        x-go-custom-tag: '"json:",inline"'
```
The output looks like this:
```
Tags []string `json:"tags" json:",inline"`
```

After this PR merged, the output will be:
```
Tags []string `json:",inline"`
```

ianchen0119: Hi @casualjim 

Sorry for disturbing you.
Could you please help to review this PR or assign the committer to review it?

Many thanks.
ianchen0119: If everything looks ok, I will help to update the documentation.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) Report
Attention: Patch coverage is `95.23810%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 83.43%. Comparing base [(`9e98a1c`)](https://app.codecov.io/gh/go-swagger/go-swagger/commit/9e98a1c267c7e21e05a21874023676e584e843f8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) to head [(`e39d5cd`)](https://app.codecov.io/gh/go-swagger/go-swagger/commit/e39d5cd12ef242cbd75ee0010a6ea258692e7f07?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger).

| [Files with missing lines](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) | Patch % | Lines |
|---|---|---|
| [generator/model.go](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122?src=pr&el=tree&filepath=generator%2Fmodel.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger#diff-Z2VuZXJhdG9yL21vZGVsLmdv) | 88.88% | [1 Missing :warning: ](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #3122   +/-   ##
=======================================
  Coverage   83.43%   83.43%           
=======================================
  Files          62       62           
  Lines       10350    10350           
=======================================
  Hits         8636     8636           
  Misses       1186     1186           
  Partials      528      528           
```

| [Flag](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) | Coverage Δ | |
|---|---|---|
| [unit-oldstable](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) | `83.43% <95.23%> (ø)` | |
| [unit-stable](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/go-swagger/go-swagger/pull/3122?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-swagger).


Patch:
diff --git a/generator/model.go b/generator/model.go
index ca5b87a0..1e3010d5 100644
--- a/generator/model.go
+++ b/generator/model.go
@@ -2059,22 +2059,23 @@ func (sg *schemaGenContext) makeGenSchema() error {
 			sg.GenSchema.ExtraImports[alias] = pkg
 		}
 
-		if !tpe.IsEmbedded {
-			sg.GenSchema.resolvedType = tpe
-			sg.GenSchema.Required = sg.Required
-			// assume we validate everything but interface and io.Reader - validation may be disabled by using the noValidation hint
-			sg.GenSchema.HasValidations = !(tpe.IsInterface || tpe.IsStream || tpe.SkipExternalValidation)
-			sg.GenSchema.IsAliased = sg.GenSchema.HasValidations
-
-			log.Printf("INFO: type %s is external, with inferred spec type %s, referred to as %s", sg.GenSchema.Name, sg.GenSchema.GoType, extType)
-			sg.GenSchema.GoType = extType
-			sg.GenSchema.AliasedType = extType
-
-			// short circuit schema building for external types
-			return nil
+		if tpe.IsEmbedded {
+			sg.GenSchema.IsAnonymous = true
 		}
-		// TODO: case for embedded types as anonymous definitions
-		return fmt.Errorf("ERROR: inline definitions embedded types are not supported")
+
+		sg.GenSchema.resolvedType = tpe
+		sg.GenSchema.Required = sg.Required
+		// assume we validate everything but interface and io.Reader - validation may be disabled by using the noValidation hint
+		sg.GenSchema.HasValidations = !(tpe.IsInterface || tpe.IsStream || tpe.SkipExternalValidation)
+		sg.GenSchema.IsAliased = sg.GenSchema.HasValidations
+
+		log.Printf("INFO: type %s is external, with inferred spec type %s, referred to as %s", sg.GenSchema.Name, sg.GenSchema.GoType, extType)
+		log.Printf("INFO: pkg: %s, alias: %s", pkg, alias)
+		sg.GenSchema.GoType = extType
+		sg.GenSchema.AliasedType = extType
+
+		// short circuit schema building for external types
+		return nil
 	}
 
 	debugLog("gschema nullable: %t", sg.GenSchema.IsNullable)
diff --git a/generator/structs.go b/generator/structs.go
index 14522888..85d52765 100644
--- a/generator/structs.go
+++ b/generator/structs.go
@@ -82,6 +82,7 @@ type GenSchema struct {
 	StrictAdditionalProperties bool
 	ReadOnly                   bool
 	IsVirtual                  bool
+	IsAnonymous                bool
 	IsBaseType                 bool
 	HasBaseType                bool
 	IsSubType                  bool
@@ -139,6 +140,8 @@ func (g GenSchema) PrintTags() string {
 		orderedTags = append(orderedTags, "xml")
 	}
 
+	customMap := getCustomTagKeyMap(g.CustomTag)
+
 	// Add extra struct tags, only if the tag hasn't already been set, i.e. example.
 	// Extra struct tags have the same value has the `json` tag.
 	for _, tag := range g.StructTags {
@@ -154,12 +157,29 @@ func (g GenSchema) PrintTags() string {
 		case tag == "description" && len(g.Description) > 0:
 			tags["description"] = g.Description
 		default:
+			if customMap[tag] {
+				// dedupe
+				continue
+			}
 			tags[tag] = tags["json"]
 		}
 
 		orderedTags = append(orderedTags, tag)
 	}
 
+	if customMap["json"] {
+		// avoid duplication
+		delete(tags, "json")
+		var newOrderedTags []string
+		for i, tag := range orderedTags {
+			if tag == "json" {
+				continue
+			}
+			newOrderedTags = append(newOrderedTags, orderedTags[i])
+		}
+		orderedTags = newOrderedTags
+	}
+
 	// Assemble the tags in key value pairs with the value properly quoted.
 	kvPairs := make([]string, 0, len(orderedTags)+1)
 	for _, key := range orderedTags {
diff --git a/generator/templates/structfield.gotmpl b/generator/templates/structfield.gotmpl
index c8e235dc..ed12478d 100644
--- a/generator/templates/structfield.gotmpl
+++ b/generator/templates/structfield.gotmpl
@@ -3,7 +3,13 @@
     // {{ template "docstring" . }}
     {{- template "propertyValidationDocString" .}}
   {{- end}}
+{{ if not $.IsAnonymous }}
 {{ pascalize .Name}} {{ template "schemaType" . }} {{ .PrintTags }}
+{{ else }}
+  {{- if and (not .IsMap) .IsNullable (not .IsSuperAlias) }}*{{ end }}
+  {{- if .IsSuperAlias }} = {{ end }}
+  {{- .GoType }} {{ .PrintTags }}
+{{ end }}
 {{ end }}
 
 {{- define "tuplefield" }}
diff --git a/generator/types.go b/generator/types.go
index 5cc00f24..60c6d49f 100644
--- a/generator/types.go
+++ b/generator/types.go
@@ -152,7 +152,8 @@ func (t typeResolver) knownDefGoType(def string, schema spec.Schema, clear func(
 		def = nm
 	}
 	extType, isExternalType := t.resolveExternalType(ext)
-	if !isExternalType || extType.Embedded {
+	// if !isExternalType || extType.Embedded {
+	if !isExternalType {
 		if clear == nil {
 			debugLog("known def type no clear: %q", def)
 			return def, t.definitionPkg, ""
diff --git a/generator/util.go b/generator/util.go
new file mode 100644
index 00000000..9f87ec2d
--- /dev/null
+++ b/generator/util.go
@@ -0,0 +1,21 @@
+package generator
+
+import "strings"
+
+func getCustomTagKeyMap(tagStr string) map[string]bool {
+	res := map[string]bool{}
+	for {
+		output := strings.SplitN(tagStr, ":", 2)
+		if len(output) != 2 {
+			break
+		}
+		res[output[0]] = true
+		output = strings.SplitN(output[1], " ", 2)
+		if len(output) != 2 {
+			break
+		}
+		tagStr = output[1]
+	}
+
+	return res
+}


Test patch:
diff --git a/generator/model_test.go b/generator/model_test.go
index 81ef09a6..6b461567 100644
--- a/generator/model_test.go
+++ b/generator/model_test.go
@@ -178,6 +178,8 @@ func TestGenerateModel_SchemaField(t *testing.T) {
 	gmp.MinItems = &in2
 	gmp.UniqueItems = true
 	gmp.ReadOnly = true
+	gmp.CustomTag = "mytag:\"foobar,foobaz\" json:\",inline\""
+	require.NotNil(t, getCustomTagKeyMap(gmp.CustomTag))
 	gmp.StructTags = []string{"json", "db", "example"}
 	gmp.Example = "some example\""
 	tt.assertRender(&gmp, `// The title of the property
@@ -194,9 +196,10 @@ func TestGenerateModel_SchemaField(t *testing.T) {
 // Max Items: 30
 // Min Items: 30
 // Unique: true
-`+"SomeName string `json:\"some name\" db:\"some name\" example:\"some example\\\"\" mytag:\"foobar,foobaz\"`\n")
+`+"SomeName string `db:\"some name\" example:\"some example\\\"\" mytag:\"foobar,foobaz\" json:\",inline\"`\n")
 
 	gmp.Example = "some example``"
+	gmp.CustomTag = "db:\",inline\" mytag:\"foobar,foobaz\" json:\",inline\""
 	tt.assertRender(&gmp, `// The title of the property
 //
 // The description of the property
@@ -211,7 +214,24 @@ func TestGenerateModel_SchemaField(t *testing.T) {
 // Max Items: 30
 // Min Items: 30
 // Unique: true
-`+"SomeName string \"json:\\\"some name\\\" db:\\\"some name\\\" example:\\\"some example``\\\" mytag:\\\"foobar,foobaz\\\"\"\n")
+`+"SomeName string \"example:\\\"some example``\\\" db:\\\",inline\\\" mytag:\\\"foobar,foobaz\\\" json:\\\",inline\\\"\"\n")
+
+	gmp.IsAnonymous = true
+	tt.assertRender(&gmp, `// The title of the property
+//
+// The description of the property
+// Example: some example`+"``"+`
+// Required: true
+// Read Only: true
+// Maximum: < 10
+// Minimum: > 10
+// Max Length: 20
+// Min Length: 20
+// Pattern: \w[\w- ]+
+// Max Items: 30
+// Min Items: 30
+// Unique: true
+`+"string \"example:\\\"some example``\\\" db:\\\",inline\\\" mytag:\\\"foobar,foobaz\\\" json:\\\",inline\\\"\"\n")
 }
 
 var schTypeGenDataSimple = []struct {