Repository: danielgtaylor/huma
Pull request #485 (2024-06-20 09:03:27)

fix(schema): proper array minItems, maxItems and doc reporting in generated schemas
Hi!

We have a use case that can be summarised by:
```golang
// sRGB color space, normalized : R,G,B,A
type Color4f [4]float64

func (c Color4f) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
	s.Description = "RGBA floating point color such as semi-transparent white is [1.0,1.0,1.0,0.5]"
	return s
}

type RequestParams struct {
	OptionalColor           *Color4f     `json:"optionalColor,omitempty"`
	RequiredColor          Color4f       `json:"requiredColor"`
}

```

Expected schema:
``` json
    "RequestParams": {
        "additionalProperties": false,
        "properties": {
          "optionalColor": {
            "description": "RGBA floating point color such as semi-transparent white is [1.0,1.0,1.0,0.5]",
            "items": {
              "format": "double",
              "type": "number"
            },
            "maxItems": 4,
            "minItems": 4,
            "type": "array"
          },
          "requiredColor": {
            "description": "RGBA floating point color such as semi-transparent white is [1.0,1.0,1.0,0.5]",
            "items": {
              "format": "double",
              "type": "number"
            },
            "maxItems": 4,
            "minItems": 4,
            "type": "array"
          }
        },
        "required": [
          "requiredColor"
        ],
        "type": "object"
      }
```
Actual schema:
```json
     "RequestParams": {
        "additionalProperties": false,
        "properties": {
          "optionalColor": {
            "items": {
              "format": "double",
              "type": "number"
            },
            "type": "array"
          },
          "requiredColor": {
            "description": "RGBA floating point color such as semi-transparent white is [1.0,1.0,1.0,0.5]",
            "items": {
              "format": "double",
              "type": "number"
            },
            "type": "array"
          }
        },
        "required": [
          "requiredColor"
        ],
        "type": "object"
      }
```

We're currently missing the minItems and maxItems for both fields and the custom description is missing if the field is a pointer to the Color4f type.

This PR try to address this issue.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Improved schema processing to handle pointers to arrays more efficiently.
	- Enhanced schema generation to support `minItems` and `maxItems` tags for better validation.

- **Tests**
	- Added tests for custom arrays and pointer arrays with descriptions to ensure robust schema validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Patch:
diff --git a/registry.go b/registry.go
index 6f00888..5b034d9 100644
--- a/registry.go
+++ b/registry.go
@@ -71,6 +71,11 @@ func (r *mapRegistry) Schema(t reflect.Type, allowRef bool, hint string) *Schema
 	origType := t
 	t = deref(t)
 
+	// Pointer to array should decay to array
+	if t.Kind() == reflect.Array || t.Kind() == reflect.Slice {
+		origType = t
+	}
+
 	alias, ok := r.aliases[t]
 	if ok {
 		return r.Schema(alias, allowRef, hint)
diff --git a/schema.go b/schema.go
index f947188..f65de67 100644
--- a/schema.go
+++ b/schema.go
@@ -559,8 +559,12 @@ func SchemaFromField(registry Registry, f reflect.StructField, hint string) *Sch
 	fs.MaxLength = intTag(f, "maxLength")
 	fs.Pattern = f.Tag.Get("pattern")
 	fs.PatternDescription = f.Tag.Get("patternDescription")
-	fs.MinItems = intTag(f, "minItems")
-	fs.MaxItems = intTag(f, "maxItems")
+	if _, ok := f.Tag.Lookup("minItems"); ok {
+		fs.MinItems = intTag(f, "minItems")
+	}
+	if _, ok := f.Tag.Lookup("maxItems"); ok {
+		fs.MaxItems = intTag(f, "maxItems")
+	}
 	fs.UniqueItems = boolTag(f, "uniqueItems")
 	fs.MinProperties = intTag(f, "minProperties")
 	fs.MaxProperties = intTag(f, "maxProperties")


Test patch:
diff --git a/schema_test.go b/schema_test.go
index 3623a7d..f92740e 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -66,6 +66,13 @@ func (c BadRefSchema) Schema(r huma.Registry) *huma.Schema {
 
 var _ huma.SchemaProvider = BadRefSchema{}
 
+type TypedArrayWithCustomDesc [4]float64
+
+func (t *TypedArrayWithCustomDesc) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
+	s.Description = "custom description"
+	return s
+}
+
 func TestSchema(t *testing.T) {
 	bitSize := strconv.Itoa(bits.UintSize)
 
@@ -704,9 +711,12 @@ func TestSchema(t *testing.T) {
 				"properties":{
 					"array":{
 						"items":{
-							"$ref":"#/components/schemas/RecursiveChildLoop"},
-							"type":"array"
+							"$ref":"#/components/schemas/RecursiveChildLoop"
 						},
+						"maxItems":1,
+						"minItems":1,
+						"type":"array"
+					},
 					"byRef":{
 						"$ref":"#/components/schemas/RecursiveChildKey"
 					},
@@ -836,6 +846,52 @@ func TestSchema(t *testing.T) {
 			}{},
 			panics: `nullable is not supported for field 'Value' which is type '#/components/schemas/ValueStruct'`,
 		},
+		{
+			name: "field-custom-array",
+			input: struct {
+				Value TypedArrayWithCustomDesc `json:"value"`
+			}{},
+			expected: ` {
+				"additionalProperties":false,
+				"properties":{
+					"value":{
+						"description":"custom description",
+						"items":{
+							"format":"double",
+							"type":"number"
+						},
+						"maxItems":4,
+						"minItems":4,
+						"type":"array"
+					}
+				},
+				"required":["value"],
+				"type":"object"
+			}`,
+		},
+		{
+			name: "field-ptr-to-custom-array",
+			input: struct {
+				Value *TypedArrayWithCustomDesc `json:"value"`
+			}{},
+			expected: ` {
+				"additionalProperties":false,
+				"properties":{
+					"value":{
+						"description":"custom description",
+						"items":{
+							"format":"double",
+							"type":"number"
+						},
+						"maxItems":4,
+						"minItems":4,
+						"type":"array"
+					}
+				},
+				"required":["value"],
+				"type":"object"
+			}`,
+		},
 	}
 
 	for _, c := range cases {