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

Issue:
Validation code for `maxProperties` is generated incorrectly
## Problem statement

When using the `maxProperties` property in a schema, then generated validation logic incorrectly checks the length of the base object as opposed to the length of the property that's being validated.

```go
func (m *SomeThing) validateData(formats strfmt.Registry) error {
	if swag.IsZero(m.Data) { // not required
		return nil
	}

	nprops := len(m) // <-- ERROR: len(m) should be len(m.Data)

	// maxProperties: 20
	if nprops > 20 {
		return errors.TooManyProperties("data", "body", 20)
	}

	return nil
}
```

## Swagger specification

```
    "SomeThing": {
      "properties": {
        "data": {
          "additionalProperties": {
            "type": "object"
          },
          "maxProperties": 20,
          "type": "object",
          "x-nullable": true
        }
      },
      "type": "object"
    },
```

## Steps to reproduce
1. Create a schema with a component schema that has the above content
2. Generate models `swagger generate models -f ./spec.json -t ./out`
3. Generated code is non-compilable.

## Environment
swagger version: v0.26.1
go version: go1.16.4 darwin/amd64
OS:  MacOS


Pull request:
fix(validations): fixed MinProperties/MaxProperties
* fixes #2587

This PR fixes some cases in which the Min/MaxProperties were wrongly evaluated:
* case of a property which is a map (issue #2587)
* case of a property which is an array  (non reported issue)
* edge case of a map representing an object with additionalProperties: false and a enum validation

Other fixes:
* fixed duplicate or missing Min/MaxProperties documentation in docstrings
* whenever a min/maxProperties validation is present, if no additional properties key is provided, assume "additionalProperties: true"

* Tests
  * added more assertions for existing test cases involving min/max properties (#2444, #2163)
  * added codegen assertions with an extended version of the spec provided with issue #2587

This PR introduces some context to GenSchema, namely IsElem and IsProperty, to allow templates to take better informed decisions. At this moment, this is only used by the minmaxproperties validation template.

Patch:
diff --git a/fixtures/bugs/2587/2587.yaml b/fixtures/bugs/2587/2587.yaml
new file mode 100644
index 00000000..4b7e173e
--- /dev/null
+++ b/fixtures/bugs/2587/2587.yaml
@@ -0,0 +1,65 @@
+---
+swagger: "2.0"
+info:
+  description: 'repro issue #2587'
+  title: maxProperties
+  version: "0.0.0"
+schemes:
+  - http
+definitions:
+  SomeThing:
+    type: object
+    properties:
+      data:
+        type: object
+        additionalProperties:
+          type: object
+        maxProperties: 20
+        x-nullable: true
+  # x-nullable has no effect
+  NestedThing:
+    type: object
+    properties:
+      data:
+        type: object
+        additionalProperties: # <- is remapped as map[string]interface{}: ok
+          type: object
+        minProperties: 15
+        maxProperties: 20
+    minProperties: 5  # <- should apply validations here: ok now (implied)
+    maxProperties: 10
+
+  SomeTypedThing:
+    type: object
+    properties:
+      data:
+        type: object
+        additionalProperties:
+          type: string
+        maxProperties: 20
+  BasicThing: # <- ok
+    type: object
+    properties:
+      data: # <- interface{}: ok
+        type: object
+    additionalProperties: true
+    maxProperties: 20
+
+  #AliasedThing: # <- don't know how to get is aliased on map
+  #  type: object
+  #  additionalProperties:
+
+  #EmbeddedThing:
+  #  $ref: '#/definitions/SomeThing'
+  #EmbeddedMap:
+  #  $ref: '#/definitions/NestedThing'
+paths:
+paths:
+  /test:
+    get:
+      responses:
+        200:
+          description: ok
+          schema:
+            $ref: '#/definitions/SomeThing'
+
diff --git a/fixtures/enhancements/2163/fixture-2163.yaml b/fixtures/enhancements/2163/fixture-2163.yaml
index b7ea5404..0ec366ca 100644
--- a/fixtures/enhancements/2163/fixture-2163.yaml
+++ b/fixtures/enhancements/2163/fixture-2163.yaml
@@ -38,10 +38,15 @@
           minProperties: 12
         e:
           type: object
-          maximum: 15
-          minProperties: 12
-          uniqueItems: true
+          maximum: 15        # <- skipped
+          minProperties: 12  # <- implies additional properties
+          uniqueItems: true  # <- incompatible: skipped
           enum: [ {"x": 1} ]
+        ebis:
+          type: object
+          additionalProperties: false
+          minProperties: 1  # <- not skipped (even though not consistent with additionalProperties: false above)
+          enum: [ {} ] # <- skipped?
         f:
           type: array
           uniqueItems: true
@@ -57,7 +62,36 @@
           pattern: "[-]?\\d+(.\\d{1,2})?"
           maxLength: 13
           minLength: 1
-          maxProperties: 12
+          maxProperties: 12 # <- warn: not compatible, skipped
+
+        h:
+          type: array
+          items:
+            type: string
+          minItems: 4
+          uniqueItems: true
+
+        i:
+          type: array
+          items:
+            type: object
+            maxProperties: 5
+          minItems: 4
+          uniqueItems: true
+
+        j:
+          type: array
+          items:
+            type: object
+            additionalProperties:
+              type: array
+              items:
+                type: object
+                minProperties: 1
+              maxItems: 12
+            maxProperties: 5
+          minItems: 4
+          uniqueItems: true
 
         #h:
         #  type: file
diff --git a/fixtures/enhancements/2444/fixture-2244.yaml b/fixtures/enhancements/2444/fixture-2244.yaml
index 4d7ffac2..5c73a6f4 100644
--- a/fixtures/enhancements/2444/fixture-2244.yaml
+++ b/fixtures/enhancements/2444/fixture-2244.yaml
@@ -96,6 +96,30 @@ definitions:
     additionalProperties:
       type: integer
 
+  nestedMap:
+    type: object
+    minProperties: 3
+    maxProperties: 5
+    additionalProperties: # <- TODO: doesn't work
+      type: object
+      additionalProperties: true
+      minProperties: 4
+      maxProperties: 6
+
+  deeperNestedMap:
+    type: object
+    minProperties: 3
+    maxProperties: 5
+    additionalProperties: # <- TODO: doesn't work
+      type: object
+      additionalProperties:
+        type: object
+        additionalProperties: true
+        minProperties: 5
+        maxProperties: 7
+      minProperties: 4
+      maxProperties: 6
+
   arrayItemsWithMinMaxProperties:
     type: array
     items:
@@ -152,4 +176,17 @@ definitions:
         c:
           type: string
           format: date
-
+  # The following definitions don't work properly (generate interface{})
+  #allOfWithValidationOnly:
+  #  allOf:
+  #  - type: object
+  #    additionalProperties: true
+  #  - minProperties: 1
+  #allOfObjectWithValidationOnly:
+  #  allOf:
+  #  - type: object
+  #    properties:
+  #      a:
+  #        type: string
+  #    additionalProperties: true
+  #  - minProperties: 1
diff --git a/generator/model.go b/generator/model.go
index 1b8cda1e..1a20626f 100644
--- a/generator/model.go
+++ b/generator/model.go
@@ -473,6 +473,10 @@ type schemaGenContext struct {
 
 	// force to use container in inlined definitions (for deconflicting)
 	UseContainerInName bool
+	// indicates is the schema is part of a slice or a map
+	IsElem bool
+	// indicates is the schema is part of a struct
+	IsProperty bool
 }
 
 func (sg *schemaGenContext) NewSliceBranch(schema *spec.Schema) *schemaGenContext {
@@ -500,6 +504,7 @@ func (sg *schemaGenContext) NewSliceBranch(schema *spec.Schema) *schemaGenContex
 	pg.ValueExpr = pg.ValueExpr + "[" + indexVar + "]"
 	pg.Schema = *schema
 	pg.Required = false
+	pg.IsElem = true
 	if sg.IsVirtual {
 		pg.TypeResolver = sg.TypeResolver.NewWithModelName(sg.TypeResolver.ModelName)
 	}
@@ -566,6 +571,7 @@ func (sg *schemaGenContext) NewStructBranch(name string, schema spec.Schema) *sc
 	pg.Name = name
 	pg.ValueExpr = pg.ValueExpr + "." + pascalize(goName(&schema, name))
 	pg.Schema = schema
+	pg.IsProperty = true
 	for _, fn := range sg.Schema.Required {
 		if name == fn {
 			pg.Required = true
@@ -621,6 +627,7 @@ func (sg *schemaGenContext) NewAdditionalProperty(schema spec.Schema) *schemaGen
 	if sg.Path != "" {
 		pg.Path = sg.Path + "+\".\"+" + pg.KeyVar
 	}
+	pg.IsElem = true
 	// propagates the special IsNullable override for maps of slices and
 	// maps of aliased types.
 	pg.GenSchema.IsMapNullOverride = sg.GenSchema.IsMapNullOverride
@@ -1228,7 +1235,13 @@ func (mt *mapStack) Dict() map[string]interface{} {
 
 func (sg *schemaGenContext) buildAdditionalProperties() error {
 	if sg.Schema.AdditionalProperties == nil {
-		return nil
+		if sg.Schema.MinProperties == nil && sg.Schema.MaxProperties == nil {
+			return nil
+		}
+
+		// whenever there is a validation on min/max properties and no additionalProperties is defined,
+		// we imply additionalProperties: true (corresponds to jsonschema defaults).
+		sg.Schema.AdditionalProperties = &spec.SchemaOrBool{Allows: true}
 	}
 	addp := *sg.Schema.AdditionalProperties
 
@@ -1256,7 +1269,9 @@ func (sg *schemaGenContext) buildAdditionalProperties() error {
 			sg.GenSchema.IsComplexObject = false
 			sg.GenSchema.IsMap = true
 
-			sg.GenSchema.ValueExpression += "." + swag.ToGoName(sg.Name+" additionalProperties")
+			if !sg.IsElem && !sg.IsProperty {
+				sg.GenSchema.ValueExpression += "." + swag.ToGoName(sg.Name+" additionalProperties")
+			}
 			cp := sg.NewAdditionalProperty(*addp.Schema)
 			cp.Name += "AdditionalProperties"
 			cp.Required = false
@@ -1325,7 +1340,6 @@ func (sg *schemaGenContext) buildAdditionalProperties() error {
 		if err := comprop.makeGenSchema(); err != nil {
 			return err
 		}
-
 		sg.MergeResult(comprop, false)
 		sg.GenSchema.AdditionalProperties = &comprop.GenSchema
 		sg.GenSchema.AdditionalProperties.ValueExpression = sg.GenSchema.ValueExpression + "[" + comprop.KeyVar + "]"
@@ -1983,6 +1997,8 @@ func (sg *schemaGenContext) makeGenSchema() error {
 	sg.GenSchema.StructTags = sg.StructTags
 	sg.GenSchema.ExtraImports = make(map[string]string)
 	sg.GenSchema.WantsRootedErrorPath = sg.WantsRootedErrorPath
+	sg.GenSchema.IsElem = sg.IsElem
+	sg.GenSchema.IsProperty = sg.IsProperty
 
 	var err error
 	returns, err := sg.shortCircuitNamedRef()
@@ -2092,6 +2108,8 @@ func (sg *schemaGenContext) makeGenSchema() error {
 	sg.GenSchema.IsMap = prev.IsMap
 	sg.GenSchema.IsAdditionalProperties = prev.IsAdditionalProperties
 	sg.GenSchema.IsBaseType = sg.GenSchema.HasDiscriminator
+	sg.GenSchema.IsElem = prev.IsElem
+	sg.GenSchema.IsProperty = prev.IsProperty
 
 	debugLogAsJSON("gschema nnullable:IsNullable:%t,resolver.IsNullable:%t,nullableOverride:%t",
 		sg.GenSchema.IsNullable, otn, nullableOverride, sg.Schema)
diff --git a/generator/structs.go b/generator/structs.go
index 78d38f83..de765d76 100644
--- a/generator/structs.go
+++ b/generator/structs.go
@@ -86,6 +86,8 @@ type GenSchema struct {
 	HasBaseType                bool
 	IsSubType                  bool
 	IsExported                 bool
+	IsElem                     bool // IsElem gives some context when the schema is part of an array or a map
+	IsProperty                 bool // IsProperty gives some context when the schema is a property of an object
 	DiscriminatorField         string
 	DiscriminatorValue         string
 	Discriminates              map[string]string
diff --git a/generator/templates/docstring.gotmpl b/generator/templates/docstring.gotmpl
index 8e7108be..a0a9d123 100644
--- a/generator/templates/docstring.gotmpl
+++ b/generator/templates/docstring.gotmpl
@@ -10,13 +10,13 @@
   {{- else }}
     {{- humanize .Name }}
   {{- end }}
-  {{- if or .MinProperties .MinProperties }}
+  {{- if or .MinProperties .MaxProperties }}
 //
     {{- if .MinProperties }}
-// Min Properties: {{ .MinProperties }}
+// MinProperties: {{ .MinProperties }}
     {{- end }}
     {{- if .MaxProperties }}
-// Max Properties: {{ .MaxProperties }}
+// MaxProperties: {{ .MaxProperties }}
     {{- end }}
   {{- end }}
   {{- if .Example }}
diff --git a/generator/templates/schemavalidator.gotmpl b/generator/templates/schemavalidator.gotmpl
index 22ef164b..cee8a5dd 100644
--- a/generator/templates/schemavalidator.gotmpl
+++ b/generator/templates/schemavalidator.gotmpl
@@ -295,7 +295,7 @@
           {{- if .IsInterface }}
       if {{ $validatedValues }}[{{ $keyVar }}] == nil { // not required
           {{- else }}
-      if swag.IsZero({{ $validatedValues }}[{{ $keyVar }}]) { // not required
+      if swag.IsZero({{ .ValueExpression }}) { // not required
           {{- end }}
         continue
       }
@@ -352,6 +352,7 @@
         {{- else if .IsArray }}
           {{ template "slicevalidator" . }}
         {{- else if and .IsMap (not .IsInterface) }}
+          {{ template "minmaxProperties" .}}
           {{ template "mapvalidator" . }}
           {{ if .Enum }}
       if err := {{ .ReceiverName }}.validate{{ pascalize .Name }}ValueEnum({{ path . }}, {{ printf "%q" .Location }}, {{ $validatedValues }}[{{ $keyVar }}]); err != nil {
@@ -430,6 +431,11 @@
         {{- end }}
       {{- end }}
     {{- end }}
+  {{- else if .Enum }}
+    // from map without additionalProperties
+    if err := {{ .ReceiverName }}.validate{{ pascalize .Name }}Enum({{ if .Path }}{{ .Path }}{{ else }}""{{ end }}, {{ printf "%q" .Location }}, {{ .ValueExpression }}); err != nil {
+      return err
+    }
   {{- end }}
 {{ end }}
 
@@ -490,7 +496,7 @@
 {{ define "minmaxProperties" }}
   {{- if and (or .IsMap (and .IsAdditionalProperties .HasAdditionalProperties)) (or .MinProperties .MaxProperties) }}
     {{- if and (not .IsAdditionalProperties) (not .IsInterface) (eq (len .Properties) 0) }}{{/* map only */}}
-    nprops := len({{ if and (not .IsAliased) .HasAdditionalProperties }}{{ .ReceiverName }}{{ else }}{{ .ValueExpression }}{{ end }})
+    nprops := len({{ if and .IsMap (not .IsAliased) .HasAdditionalProperties (not .IsElem) (not .IsProperty) }}{{ .ReceiverName }}{{ else }}{{ .ValueExpression }}{{ end }})
     {{- else }}{{/* object with properties */}}
       {{- if and .IsNullable .MinProperties }}
         {{- if gt0 .MinProperties }}
diff --git a/generator/templates/validation/structfield.gotmpl b/generator/templates/validation/structfield.gotmpl
index 9f2bb608..26fd47d2 100644
--- a/generator/templates/validation/structfield.gotmpl
+++ b/generator/templates/validation/structfield.gotmpl
@@ -40,14 +40,6 @@
 // Min Items: {{ .MinItems }}
 {{- end }}
 
-{{- if .MinProperties }}
-// Min Properties: {{ .MinProperties }}
-{{- end }}
-
-{{- if .MaxProperties }}
-// Max Properties: {{ .MaxProperties }}
-{{- end }}
-
 {{- if .UniqueItems }}
 // Unique: true
 {{- end }}


Test patch:
diff --git a/generator/moreschemavalidation_fixtures_test.go b/generator/moreschemavalidation_fixtures_test.go
index 61fceac8..ab95e0d0 100644
--- a/generator/moreschemavalidation_fixtures_test.go
+++ b/generator/moreschemavalidation_fixtures_test.go
@@ -173,6 +173,45 @@ func initFixture2444() {
 		`	if nprops > 5 {`,
 		`		return errors.TooManyProperties("", "body", 5)`,
 	}, todo, noLines, noLines)
+
+	flattenRun.AddExpectations("nested_map.go", []string{
+		`type NestedMap map[string]map[string]interface{}`,
+		`nprops := len(m)`,
+		`	if nprops < 3 {`,
+		`		return errors.TooFewProperties("", "body", 3)`,
+		`	if nprops > 5 {`,
+		`		return errors.TooManyProperties("", "body", 5)`,
+		`for k := range m {`,
+		`nprops := len(m[k])`,
+		`if nprops < 4 {`,
+		`	return errors.TooFewProperties(k, "body", 4)`,
+		`if nprops > 6 {`,
+		`	return errors.TooManyProperties(k, "body", 6)`,
+	}, todo, noLines, noLines)
+
+	flattenRun.AddExpectations("deeper_nested_map.go", []string{
+		`type DeeperNestedMap map[string]map[string]map[string]interface{}`,
+		`nprops := len(m)`,
+		`if nprops < 3 {`,
+		`return errors.TooFewProperties("", "body", 3)`,
+		`if nprops > 5 {`,
+		`return errors.TooManyProperties("", "body", 5)`,
+		`for k := range m {`,
+		`nprops := len(m[k])`,
+		`if nprops < 4 {`,
+		`return errors.TooFewProperties(k, "body", 4)`,
+		`if nprops > 6 {`,
+		`return errors.TooManyProperties(k, "body", 6)`,
+		`for kk := range m[k] {`,
+		`if nprops < 5 {`,
+		`return errors.TooFewProperties(k+"."+kk, "body", 5)`,
+		`if nprops > 7 {`,
+		`return errors.TooManyProperties(k+"."+kk, "body", 7)`,
+		``,
+		``,
+		``,
+		``,
+	}, todo, noLines, noLines)
 }
 
 func initFixtureGuardFormats() {
@@ -11585,3 +11624,126 @@ func initFixture2364() {
 		noLines,
 		noLines)
 }
+
+func initFixture2163() {
+	f := newModelFixture("../fixtures/enhancements/2163/fixture-2163.yaml", "ambiguous validations")
+	flattenRun := f.AddRun(false).WithMinimalFlatten(true)
+
+	flattenRun.AddExpectations("obj.go", []string{
+		`E map[string]interface{}`,
+		`Ebis map[string]interface{}`,
+		`I []map[string]interface{}`,
+		`J []map[string][]map[string]interface{}`,
+		//
+		`func (m *Obj) validateEEnum(path, location string, value map[string]interface{}) error {`,
+		`if swag.IsZero(m.E) {`,
+		`nprops := len(m.E)`,
+		`if nprops < 12 {`,
+		`return errors.TooFewProperties("e", "body", 12)`,
+		`if err := m.validateEEnum("e", "body", m.E); err != nil {`,
+		//
+		`func (m *Obj) validateEbisEnum(path, location string, value map[string]interface{}) error {`,
+		`if swag.IsZero(m.Ebis) {`,
+		`nprops := len(m.Ebis)`,
+		`if nprops < 1 {`,
+		`return errors.TooFewProperties("ebis", "body", 1)`,
+		`if err := m.validateEbisEnum("ebis", "body", m.Ebis); err != nil {`,
+		//
+		`func (m *Obj) validateF(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.F) {`,
+		`if err := validate.UniqueItems("f", "body", m.F); err != nil {`,
+		`for i := 0; i < len(m.F); i++ {`,
+		`nprops := len(m.F[i])`,
+		`if nprops < 13 {`,
+		`return errors.TooFewProperties("f"+"."+strconv.Itoa(i), "body", 13)`,
+		//
+		`func (m *Obj) validateI(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.I) {`,
+		`if err := validate.MinItems("i", "body", iISize, 4); err != nil {`,
+		`if err := validate.UniqueItems("i", "body", m.I); err != nil {`,
+		`for i := 0; i < len(m.I); i++ {`,
+		`nprops := len(m.I[i])`,
+		`if nprops > 5 {`,
+		`return errors.TooManyProperties("i"+"."+strconv.Itoa(i), "body", 5`,
+		//
+		`func (m *Obj) validateJ(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.J) {`,
+		`iJSize := int64(len(m.J))`,
+		`if err := validate.MinItems("j", "body", iJSize, 4); err != nil {`,
+		`if err := validate.UniqueItems("j", "body", m.J); err != nil {`,
+		`for i := 0; i < len(m.J); i++ {`,
+		`nprops := len(m.J[i])`,
+		`if nprops > 5 {`,
+		`return errors.TooManyProperties("j"+"."+strconv.Itoa(i), "body", 5)`,
+		`for k := range m.J[i] {`,
+		`iiJSize := int64(len(m.J[i][k]))`,
+		`if err := validate.MaxItems("j"+"."+strconv.Itoa(i)+"."+k, "body", iiJSize, 12); err != nil {`,
+		`for ii := 0; ii < len(m.J[i][k]); ii++ {`,
+		`nprops := len(m.J[i][k][ii])`,
+		`if nprops < 1 {`,
+		`return errors.TooFewProperties("j"+"."+strconv.Itoa(i)+"."+k+"."+strconv.Itoa(ii), "body", 1)`,
+	}, todo, noLines, noLines)
+}
+
+func initFixture2587() {
+	f := newModelFixture("../fixtures/bugs/2587/2587.yaml", "min/max properties")
+	flattenRun := f.AddRun(false).WithMinimalFlatten(true)
+
+	flattenRun.AddExpectations("basic_thing.go", []string{
+		`type BasicThing struct {`,
+		`Data interface{} `,
+		`func (m *BasicThing) Validate(formats strfmt.Registry) error {`,
+		`props := make(map[string]json.RawMessage, 1+10)`,
+		`j, err := swag.WriteJSON(m)`,
+		`nprops := len(props)`,
+		`if nprops > 20 {`,
+		`return errors.TooManyProperties("", "body", 20)`,
+	}, todo, noLines, noLines)
+
+	flattenRun.AddExpectations("nested_thing.go", []string{
+		`type NestedThing struct {`,
+		`Data map[string]interface{} `,
+		`NestedThingAdditionalProperties map[string]interface{}`,
+		`func (m *NestedThing) Validate(formats strfmt.Registry) error {`,
+		`// short circuits minProperties > 0`,
+		`if m == nil {`,
+		`props := make(map[string]json.RawMessage, 1+10)`,
+		`nprops := len(props)`,
+		`if nprops < 5 {`,
+		`return errors.TooFewProperties("", "body", 5)`,
+		`if nprops > 10`,
+		`return errors.TooManyProperties("", "body", 10)`,
+		`if err := m.validateData(formats); err != nil {`,
+		`func (m *NestedThing) validateData(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.Data) {`,
+		`nprops := len(m.Data)`,
+		`if nprops < 15 {`,
+		`return errors.TooFewProperties("data", "body", 15)`,
+		`if nprops > 20 {`,
+		`return errors.TooManyProperties("data", "body", 20)`,
+	}, todo, noLines, noLines)
+
+	flattenRun.AddExpectations("some_thing.go", []string{
+		`type SomeThing struct {`,
+		`Data map[string]interface{}`,
+		`func (m *SomeThing) Validate(formats strfmt.Registry) error {`,
+		`if err := m.validateData(formats); err != nil {`,
+		`func (m *SomeThing) validateData(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.Data) {`,
+		`nprops := len(m.Data)`,
+		`if nprops > 20 {`,
+		`return errors.TooManyProperties("data", "body", 20)`,
+	}, todo, noLines, noLines)
+
+	flattenRun.AddExpectations("some_typed_thing.go", []string{
+		`type SomeTypedThing struct {`,
+		`Data map[string]string`,
+		`func (m *SomeTypedThing) Validate(formats strfmt.Registry) error {`,
+		`func (m *SomeTypedThing) Validate(formats strfmt.Registry) error {`,
+		`func (m *SomeTypedThing) validateData(formats strfmt.Registry) error {`,
+		`if swag.IsZero(m.Data) {`,
+		`nprops := len(m.Data)`,
+		`if nprops > 20 {`,
+		`return errors.TooManyProperties("data", "body", 20)`,
+	}, todo, noLines, noLines)
+}
diff --git a/generator/moreschemavalidation_test.go b/generator/moreschemavalidation_test.go
index dd59129d..3ac3d058 100644
--- a/generator/moreschemavalidation_test.go
+++ b/generator/moreschemavalidation_test.go
@@ -289,6 +289,12 @@ func initModelFixtures() {
 
 	// additional cases for embedded struct
 	initFixture2604()
+
+	// ambiguous validations
+	initFixture2163()
+
+	// min / maxProperties, more cases
+	initFixture2587()
 }
 
 /* Template initTxxx() to prepare and load a fixture: