Repository: go-swagger/go-swagger (go)

Issue:
Backticks appearing in tags are not properly handled
## Problem statement

If schema data containing backticks end up in struct (etc.) tags of generated Go code, the tag generation is not handling quotation of tags properly. ( Outer quotes would need to be " instead of backtick with inner double quotes being escaped. One solution is to do this with _all_ tags, but that would make the generated code ugly,  and `go fmt` doesn't clean it up.)

## Swagger specification

Backticks are legal, e.g. in `XMLObject`, the [name](https://swagger.io/specification/v2/#xmlObject) parameter is a `string` with no further restrictions ...

## Steps to reproduce

I've attached a minimal extract from a real-world public schema (https://api.mouser.com/api/docs/ui/index) which ends up generating broken code.  Executed as (`swagger generate client -f test.json.txt`), here are the relevant bits:

```
// schema
"xml" : {
    "name" : "KeyValuePair`2",
    "wrapped" : true
}
// go IS
(...) `json:"ScheduledReleases" xml:"KeyValuePair`2"`

// go SHOULD
(...) "json:\"ScheduledReleases\" xml:\"KeyValuePair`2\""
```

[test.json.txt](https://github.com/go-swagger/go-swagger/files/5538040/test.json.txt)

Unfortunately, you can't escape backticks within backticks, so Tags need to revert to double quotes containing escaped quotation marks ...

(Also, I'm well aware that this is _terribly awfully unaesthetic_ style and it leaves me scratching my head asking myself what sort of a _deranged monster_ would do such a thing, but as far as I can tell it's perfectly legal.)

## Environment
swagger version: current (2020-11-13) master (  8831a62b )
go version: ```
go version go1.14.3 linux/amd64```
OS:   
```
$ lsb_release  -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.1 LTS
Release:	20.04
Codename:	focal
```


Pull request description:
refactored, documented and tested tags in models
* fixes #2423 - acknowledged fixed
* refactored tag rendering: use strings.Builder, cleaned up redundancies with xml tags,
  used stdlib strconv.CanBackQuote instead of Contains
* now always renders extra tags in order specified by the user (e.g. --struct-tags example,db)
* now renders xml tags if requested in the CLI opts, even when no consumes/produces is set with xml
* added unit tests for model tag generation
* added doc on how to play with struct tags

Signed-off-by: Frederic BIDON <fredbi@yahoo.com>

Patch:
diff --git a/docs/use/model.md b/docs/use/model.md
index dce40b99..e92338c4 100644
--- a/docs/use/model.md
+++ b/docs/use/model.md
@@ -38,3 +38,4 @@ See the full list of available options [here](../generate/model.md).
   * [Polymorphic types](./models/schemas.md#polymorphic-types)
   * [Serialization interfaces](./models/schemas.md#serialization-interfaces)
   * [External types](./models/schemas.md#external-types)
+  * [Customizing struct tags](./models/schemas.md#customizing-struct-tags)
diff --git a/docs/use/models/schemas.md b/docs/use/models/schemas.md
index 837ea895..64bf9181 100644
--- a/docs/use/models/schemas.md
+++ b/docs/use/models/schemas.md
@@ -196,11 +196,13 @@ Recap as of release `0.15`:
 Model generation may be altered with the following extensions:
 
 - `x-go-name: "string"`: give explicit type name to the generated model
-- `x-go-custom-tag: "string"`: add serialization tags to an object property
+- `x-go-custom-tag: "string"`: add serialization tags to an object property (see [Customizing struct tags](#customizing-struct-tags))
 - `x-nullable: true|false` (or equivalently `x-is-nullable:true|false`): accepts null values (i.e. rendered as a pointer)
 - `x-go-type: "string"`: explicitly reuse an already available go type
 - `x-class: "string"`: give explicit polymorphic class name in discriminator
 - `x-order: number`: indicates explicit generation ordering for schemas (e.g. models, properties, allOf, ...)
+- `x-omitempty: true|false`: force the omitempty modifier in struct json and xml tags
+- `x-go-json-string: true:false`: force the string modifier in struct json tags
 
 ### Primitive types
 
@@ -290,8 +292,14 @@ to manipulate pointers more easily.
 > An alternate design has been experimented but not released. For those interested in pushing forward this project again,
 > see [this pull request][lifting-pointers]
 
+#### Common use cases
+
+You don't always have to resort to pointers to figure out whether a value is empty.
+
+* The idiomatic way to check for a null/empty string is: `minLength: 1`
 
 ### Validation
+
 All produced models implement the [Validatable] interface.
 
 Exceptions:
@@ -718,12 +726,6 @@ Note that the marshalling of a base type into JSON is processed naturally, so th
 
 <!--
 
-TODO
-
-##### Serializer's tags
-
-##### Custom tags
-
 JSON
 
 XML
@@ -1191,6 +1193,122 @@ Package aliases may still conflict with packages produces by operation tags or o
 
 In such cases, modify the type alias under `x-go-type` to resolve the conflict manually.
 
+### Customizing struct tags
+
+When a model struct is generated, tags for json are generated to keep the original name:
+
+```go
+type ObjectWithTag struct {
+    StandardTag string `json:"standardTag,omitempty"`
+}
+```
+
+#### Extra tags
+
+Extra tags may be defined with the CLI generation option `--sruct-tags`.
+
+Extra tags essentially repeat the name of the field can be added from the command line option.
+
+> **NOTE**: at this moment, all tag modifiers (omitempty, string) are repeated like for the json tag.
+
+```bash
+swagger generate model ... --struct-tags yaml,db
+```
+
+```go
+type ObjectWithTag struct {
+    StandardTag string `json:"standardTag,omitempty" yaml:"standardTag,omitempty" db:"standardTag,omitempty"`
+}
+```
+
+#### Custom tags
+
+A custom may be added to a field using the `x-go-custom-tag` extension. Like so:
+
+#### Omit empty values
+
+By default, a struct field is omitted when it holds the zero value (tag modifier: `omitempty`).
+
+Required fields are never omitted.
+
+```go
+type ObjectWithTag struct {
+    RequiredField *string `json:"requiredField"`
+}
+```
+
+This property can be altered using the `x-omitempty` extension. Like so:
+```yaml
+objectWithTag:
+  type: object
+  properties:
+    field:
+      type: string
+      x-omitempty: false
+```
+
+```go
+type ObjectWithTag struct {
+    Field string `json:"field"`
+}
+```
+
+The extension does not force a required field to get the "omitempty" modifier.
+
+#### Numerical values as string
+
+For some specific requirements, the standard json library may consider numbers as strings.
+This is done by adding the modifier `json:"...,string"` to the tag.
+
+With go-swagger you can specify this modifier by adding the `x-go-json-string: true` extension to your type.
+
+```go
+type ObjectWithTag struct {
+    NumericField int `json:"field,omitempty,string"`
+}
+```
+
+#### XML tags
+
+The XML name and attribute Swagger properties are used to generate extra tags.
+```yaml
+definitions:
+ objectWithXML:
+   type: object
+   properties:
+     field:
+       type: string
+       xml:
+         name: xmlObject
+         attribute: true
+```
+
+```go
+type ObjectWithXML struct {
+    Field string `json:"field,omitempty" xml:"xmlObject,attr,omitempty"`
+}
+```
+
+#### The example tag
+
+If you add `example` to the list of generated tags from the CLI (`swagger generate ... --struct-tags example`),
+a special example tag is created with the example value taken from the specification.
+
+```yaml
+definitions:
+  objectWithExample:
+   properties:
+     field:
+       type: string
+       example: "sample"
+```
+
+```go
+type ObjectWithExample struct {
+    Field string `json:"field,omitempty" example:"\"sample\""`
+}
+```
+
 
 [swagger]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schema-object
 [strfmt]: https://github.com/go-openapi/strfmt
diff --git a/generator/model.go b/generator/model.go
index 21d1ffb9..8213e2d6 100644
--- a/generator/model.go
+++ b/generator/model.go
@@ -1654,7 +1654,11 @@ func (sg *schemaGenContext) buildAdditionalItems() error {
 }
 
 func (sg *schemaGenContext) buildXMLNameWithTags() error {
-	if sg.WithXML || sg.Schema.XML != nil {
+	// render some "xml" struct tag under one the following conditions:
+	// - consumes/produces in spec contains xml
+	// - struct tags CLI option contains xml
+	// - XML object present in spec for this schema
+	if sg.WithXML || swag.ContainsStrings(sg.StructTags, "xml") || sg.Schema.XML != nil {
 		sg.GenSchema.XMLName = sg.Name
 
 		if sg.Schema.XML != nil {
@@ -1665,10 +1669,6 @@ func (sg *schemaGenContext) buildXMLNameWithTags() error {
 				sg.GenSchema.XMLName += ",attr"
 			}
 		}
-
-		if !sg.GenSchema.Required && sg.GenSchema.IsEmptyOmitted {
-			sg.GenSchema.XMLName += ",omitempty"
-		}
 	}
 	return nil
 }
diff --git a/generator/structs.go b/generator/structs.go
index e16c877c..d3098b25 100644
--- a/generator/structs.go
+++ b/generator/structs.go
@@ -9,7 +9,6 @@ import (
 	"strings"
 
 	"github.com/go-openapi/spec"
-	"github.com/go-openapi/swag"
 )
 
 // GenCommon contains common properties needed across
@@ -97,55 +96,63 @@ type GenSchema struct {
 	ExtraImports               map[string]string // non-standard imports detected when using external types
 }
 
-func (g *GenSchema) renderMarshalTag() string {
+func (g GenSchema) renderMarshalTag() string {
 	if g.HasBaseType {
 		return "-"
 	}
 
-	result := g.OriginalName
+	var result strings.Builder
+
+	result.WriteString(g.OriginalName)
 
 	if !g.Required && g.IsEmptyOmitted {
-		result += ",omitempty"
+		result.WriteString(",omitempty")
 	}
 
 	if g.IsJSONString {
-		result += ",string"
+		result.WriteString(",string")
 	}
 
-	return result
+	return result.String()
 }
 
 // PrintTags takes care of rendering tags for a struct field
-func (g *GenSchema) PrintTags() string {
-	tags := make(map[string]string)
-	orderedTags := make([]string, 0, 2)
+func (g GenSchema) PrintTags() string {
+	tags := make(map[string]string, 3)
+	orderedTags := make([]string, 0, 3)
 
 	tags["json"] = g.renderMarshalTag()
 	orderedTags = append(orderedTags, "json")
 
 	if len(g.XMLName) > 0 {
-		tags["xml"] = g.XMLName
+		if !g.Required && g.IsEmptyOmitted {
+			tags["xml"] = g.XMLName + ",omitempty"
+		} else {
+			tags["xml"] = g.XMLName
+		}
 		orderedTags = append(orderedTags, "xml")
 	}
 
-	// Only add example tag if it's contained in the struct tags.
-	if len(g.Example) > 0 && swag.ContainsStrings(g.StructTags, "example") {
-		tags["example"] = g.Example
-		orderedTags = append(orderedTags, "example")
-	}
-
 	// 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 {
-		// TODO: example handling is not very clean
-		if _, exists := tags[tag]; !exists && tag != "example" {
+		if _, exists := tags[tag]; exists {
+			// dedupe
+			continue
+		}
+
+		if tag == "example" && len(g.Example) > 0 {
+			// only add example tag if it's contained in the struct tags
+			tags["example"] = g.Example // json representation of the example object
+		} else {
 			tags[tag] = tags["json"]
-			orderedTags = append(orderedTags, tag)
 		}
+
+		orderedTags = append(orderedTags, tag)
 	}
 
 	// Assemble the tags in key value pairs with the value properly quoted.
-	kvPairs := make([]string, 0, len(tags)+1)
+	kvPairs := make([]string, 0, len(orderedTags)+1)
 	for _, key := range orderedTags {
 		kvPairs = append(kvPairs, fmt.Sprintf("%s:%s", key, strconv.Quote(tags[key])))
 	}
@@ -161,7 +168,7 @@ func (g *GenSchema) PrintTags() string {
 	// escaping backticks in raw string literals.
 	valuesHaveBacktick := false
 	for _, value := range tags {
-		if strings.Contains(value, "`") {
+		if !strconv.CanBackquote(value) {
 			valuesHaveBacktick = true
 			break
 		}
@@ -170,6 +177,7 @@ func (g *GenSchema) PrintTags() string {
 	if !valuesHaveBacktick {
 		return fmt.Sprintf("`%s`", completeTag)
 	}
+
 	// We have to escape the tag again to put it in a literal with double quotes as the tag format uses double quotes.
 	return strconv.Quote(completeTag)
 }


Test patch:
diff --git a/generator/model_test.go b/generator/model_test.go
index cadafcbe..b349c17b 100644
--- a/generator/model_test.go
+++ b/generator/model_test.go
@@ -195,7 +195,7 @@ func TestGenerateModel_SchemaField(t *testing.T) {
 // Max Items: 30
 // Min Items: 30
 // Unique: true
-`+"SomeName string `json:\"some name\" example:\"some example\\\"\" db:\"some name\" mytag:\"foobar,foobaz\"`\n")
+`+"SomeName string `json:\"some name\" db:\"some name\" example:\"some example\\\"\" mytag:\"foobar,foobaz\"`\n")
 
 	gmp.Example = "some example``"
 	tt.assertRender(&gmp, `// The title of the property
@@ -212,7 +212,7 @@ func TestGenerateModel_SchemaField(t *testing.T) {
 // Max Items: 30
 // Min Items: 30
 // Unique: true
-`+"SomeName string \"json:\\\"some name\\\" example:\\\"some example``\\\" db:\\\"some name\\\" mytag:\\\"foobar,foobaz\\\"\"\n")
+`+"SomeName string \"json:\\\"some name\\\" db:\\\"some name\\\" example:\\\"some example``\\\" mytag:\\\"foobar,foobaz\\\"\"\n")
 }
 
 var schTypeGenDataSimple = []struct {
diff --git a/generator/structs_test.go b/generator/structs_test.go
new file mode 100644
index 00000000..15bc4bf5
--- /dev/null
+++ b/generator/structs_test.go
@@ -0,0 +1,160 @@
+package generator
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestPrintTags(t *testing.T) {
+	type tagFixture struct {
+		Title        string
+		Schema       GenSchema
+		ExpectedTags string
+	}
+
+	mustJSON := func(in interface{}) string {
+		b, _ := asJSON(in)
+		return b
+	}
+
+	fixtures := []tagFixture{
+		{
+			Title: "no extra: default json",
+			Schema: GenSchema{
+				OriginalName: "field",
+			},
+			ExpectedTags: "`json:\"field\"`",
+		},
+		{
+			Title: "no extra: default json, omitempty",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+			},
+			ExpectedTags: "`json:\"field,omitempty\"`",
+		},
+		{
+			Title: "no extra: default json, required, omitempty",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				sharedValidations: sharedValidations{
+					Required: true,
+				},
+			},
+			ExpectedTags: "`json:\"field\"`",
+		},
+		{
+			Title: "no extra: default json, omitempty, as JSON string",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+					IsJSONString:   true,
+				},
+			},
+			ExpectedTags: "`json:\"field,omitempty,string\"`",
+		},
+		{
+			Title: "with xml name",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				sharedValidations: sharedValidations{
+					Required: true,
+				},
+				XMLName: "xmlfield",
+			},
+			ExpectedTags: "`json:\"field\" xml:\"xmlfield\"`",
+		},
+		{
+			Title: "with example (1/3)",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				sharedValidations: sharedValidations{
+					Required: true,
+				},
+				Example:    mustJSON("xyz"),
+				StructTags: []string{"example"},
+			},
+			ExpectedTags: "`json:\"field\" example:\"\\\"xyz\\\"\"`",
+		},
+		{
+			Title: "with example (2/3)",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				sharedValidations: sharedValidations{
+					Required: true,
+				},
+				Example:    mustJSON(15),
+				StructTags: []string{"example"},
+			},
+			ExpectedTags: "`json:\"field\" example:\"15\"`",
+		},
+		{
+			Title: "with example (3/3)",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				sharedValidations: sharedValidations{
+					Required: true,
+				},
+				Example: mustJSON(struct {
+					A string `json:"a"`
+					B int64  `json:"b"`
+				}{A: "xyz", B: 12}),
+				StructTags: []string{"example"},
+			},
+			ExpectedTags: "`json:\"field\" example:\"{\\\"a\\\":\\\"xyz\\\",\\\"b\\\":12}\"`",
+		},
+		{
+			Title: "with example, xml, omitempty, custom tag",
+			Schema: GenSchema{
+				OriginalName: "field",
+				resolvedType: resolvedType{
+					IsEmptyOmitted: true,
+				},
+				Example:    mustJSON(15),
+				StructTags: []string{"example"},
+				XMLName:    "xmlfield,attr",
+				CustomTag:  `metric:"on"`,
+			},
+			ExpectedTags: "`json:\"field,omitempty\" xml:\"xmlfield,attr,omitempty\" example:\"15\" metric:\"on\"`",
+		},
+		{
+			Title: "with backticks",
+			Schema: GenSchema{
+				OriginalName: "field",
+				Example: mustJSON(struct {
+					A string `json:"a"`
+					B int64  `json:"b"`
+				}{A: "`xyz`", B: 12}),
+				StructTags: []string{"example"},
+				CustomTag:  "metric:\"`on`\"",
+			},
+			ExpectedTags: "\"json:\\\"field\\\" example:\\\"{\\\\\\\"a\\\\\\\":\\\\\\\"`xyz`\\\\\\\",\\\\\\\"b\\\\\\\":12}\\\" metric:\\\"`on`\\\"\"",
+		},
+	}
+
+	for _, toPin := range fixtures {
+		fixture := toPin
+		t.Run(fixture.Title, func(t *testing.T) {
+			require.Equal(t, fixture.ExpectedTags, fixture.Schema.PrintTags())
+		})
+	}
+}