Repository: getkin/kin-openapi
Pull request #145 (2019-11-20 23:53:13)

Fix panic when verify array unique items(#143)
json.Unmarshal will store JSON object as map[string] interface{}
and store JSON array as []interface{}. Due to map and slice can
not be the key of map, so if the array items' type is JSON object
or JSON array, verify unique items of array will panic due map
and slice are unhashable.

fenollp: Fixes https://github.com/getkin/kin-openapi/issues/143

Patch:
diff --git a/README.md b/README.md
index 912faf2..cc528e6 100644
--- a/README.md
+++ b/README.md
@@ -156,3 +156,41 @@ func xmlBodyDecoder(body []byte) (interface{}, error) {
 	// Decode body to a primitive, []inteface{}, or map[string]interface{}.
 }
 ```
+
+## Custom function for check uniqueness of JSON array
+
+By defaut, the library check unique items by below predefined function
+
+```go
+func isSliceOfUniqueItems(xs []interface{}) bool {
+	s := len(xs)
+	m := make(map[string]struct{}, s)
+	for _, x := range xs {
+		// The input slice is coverted from a JSON string, there shall
+		// have no error when covert it back.
+		key, _ := json.Marshal(&x)
+		m[string(key)] = struct{}{}
+	}
+	return s == len(m)
+}
+```
+
+In the predefined function using `json.Marshal` to generate a string can
+be used as a map key which is to support check the uniqueness of an array
+when the array items are JSON objects or JSON arraies. You can register
+you own function according to your input data to get better performance:
+
+```go
+func main() {
+	// ...
+
+	// Register a customized function used to check uniqueness of array.
+	openapi3.RegisterArrayUniqueItemsChecker(arrayUniqueItemsChecker)
+
+	// ... other validate codes
+}
+
+func arrayUniqueItemsChecker(items []interface{}) bool {
+	// Check the uniqueness of the input slice(array in JSON)
+}
+```
\ No newline at end of file
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 4a366a8..d01c492 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -982,7 +982,7 @@ func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error)
 	}
 
 	// "uniqueItems"
-	if v := schema.UniqueItems; v && !isSliceOfUniqueItems(value) {
+	if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
 		if fast {
 			return errSchema
 		}
@@ -1221,13 +1221,31 @@ func (err *SchemaError) Error() string {
 
 func isSliceOfUniqueItems(xs []interface{}) bool {
 	s := len(xs)
-	m := make(map[interface{}]struct{}, s)
+	m := make(map[string]struct{}, s)
 	for _, x := range xs {
-		m[x] = struct{}{}
+		// The input slice is coverted from a JSON string, there shall
+		// have no error when covert it back.
+		key, _ := json.Marshal(&x)
+		m[string(key)] = struct{}{}
 	}
 	return s == len(m)
 }
 
+// SliceUniqueItemsChecker is an function used to check if an given slice
+// have unique items.
+type SliceUniqueItemsChecker func(items []interface{}) bool
+
+// By default using predefined func isSliceOfUniqueItems which make use of
+// json.Marshal to generate a key for map used to check if a given slice
+// have unique items.
+var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems
+
+// RegisterArrayUniqueItemsChecker is used to register a customized function
+// used to check if JSON array have unique items.
+func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) {
+	sliceUniqueItemsChecker = fn
+}
+
 func unsupportedFormat(format string) error {
 	return fmt.Errorf("Unsupported 'format' value '%s'", format)
 }
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 40d6bf6..62d314a 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -16,6 +16,12 @@ import (
 // ErrInvalidRequired is an error that happens when a required value of a parameter or request's body is not defined.
 var ErrInvalidRequired = errors.New("must have a value")
 
+// ValidateRequest is used to validate the given input according to previous
+// loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a
+// non-nil error will be returned.
+//
+// Note: One can tune the behavior of uniqueItems: true verification
+// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
 func ValidateRequest(c context.Context, input *RequestValidationInput) error {
 	options := input.Options
 	if options == nil {
diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go
index ce376ce..dad0864 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -11,6 +11,12 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
+// ValidateResponse is used to validate the given input according to previous
+// loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a
+// non-nil error will be returned.
+//
+// Note: One can tune the behavior of uniqueItems: true verification
+// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
 func ValidateResponse(c context.Context, input *ResponseValidationInput) error {
 	req := input.RequestValidationInput.Request
 	switch req.Method {


Test patch:
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index ba365eb..1237f9d 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -355,6 +355,296 @@ var schemaExamples = []schemaExample{
 			},
 		},
 	},
+	{
+		Title: "ARRAY : items format 'object'",
+		Schema: &openapi3.Schema{
+			Type:        "array",
+			UniqueItems: true,
+			Items: (&openapi3.Schema{
+				Type: "object",
+				Properties: map[string]*openapi3.SchemaRef{
+					"key1": openapi3.NewFloat64Schema().NewRef(),
+				},
+			}).NewRef(),
+		},
+		Serialization: map[string]interface{}{
+			"type":        "array",
+			"uniqueItems": true,
+			"items": map[string]interface{}{
+				"properties": map[string]interface{}{
+					"key1": map[string]interface{}{
+						"type": "number",
+					},
+				},
+				"type": "object",
+			},
+		},
+		AllValid: []interface{}{
+			[]interface{}{
+				map[string]interface{}{
+					"key1": 1,
+					"key2": 1,
+					// Additioanl properties will make object different
+					// By default additionalProperties is true
+				},
+				map[string]interface{}{
+					"key1": 1,
+				},
+			},
+			[]interface{}{
+				map[string]interface{}{
+					"key1": 1,
+				},
+				map[string]interface{}{
+					"key1": 2,
+				},
+			},
+		},
+		AllInvalid: []interface{}{
+			[]interface{}{
+				map[string]interface{}{
+					"key1": 1,
+				},
+				map[string]interface{}{
+					"key1": 1,
+				},
+			},
+		},
+	},
+
+	{
+		Title: "ARRAY : items format 'object' and object with a property of array type ",
+		Schema: &openapi3.Schema{
+			Type:        "array",
+			UniqueItems: true,
+			Items: (&openapi3.Schema{
+				Type: "object",
+				Properties: map[string]*openapi3.SchemaRef{
+					"key1": (&openapi3.Schema{
+						Type:        "array",
+						UniqueItems: true,
+						Items:       openapi3.NewFloat64Schema().NewRef(),
+					}).NewRef(),
+				},
+			}).NewRef(),
+		},
+		Serialization: map[string]interface{}{
+			"type":        "array",
+			"uniqueItems": true,
+			"items": map[string]interface{}{
+				"properties": map[string]interface{}{
+					"key1": map[string]interface{}{
+						"type":        "array",
+						"uniqueItems": true,
+						"items": map[string]interface{}{
+							"type": "number",
+						},
+					},
+				},
+				"type": "object",
+			},
+		},
+		AllValid: []interface{}{
+			[]interface{}{
+				map[string]interface{}{
+					"key1": []interface{}{
+						1, 2,
+					},
+				},
+				map[string]interface{}{
+					"key1": []interface{}{
+						3, 4,
+					},
+				},
+			},
+			[]interface{}{ // Slice have items with the same value but with different index will treated as different slices
+				map[string]interface{}{
+					"key1": []interface{}{
+						10, 9,
+					},
+				},
+				map[string]interface{}{
+					"key1": []interface{}{
+						9, 10,
+					},
+				},
+			},
+		},
+		AllInvalid: []interface{}{
+			[]interface{}{ // Violate outer array uniqueItems: true
+				map[string]interface{}{
+					"key1": []interface{}{
+						9, 9,
+					},
+				},
+				map[string]interface{}{
+					"key1": []interface{}{
+						9, 9,
+					},
+				},
+			},
+			[]interface{}{ // Violate inner(array in object) array uniqueItems: true
+				map[string]interface{}{
+					"key1": []interface{}{
+						9, 9,
+					},
+				},
+				map[string]interface{}{
+					"key1": []interface{}{
+						8, 8,
+					},
+				},
+			},
+		},
+	},
+
+	{
+		Title: "ARRAY : items format 'array'",
+		Schema: &openapi3.Schema{
+			Type:        "array",
+			UniqueItems: true,
+			Items: (&openapi3.Schema{
+				Type:        "array",
+				UniqueItems: true,
+				Items:       openapi3.NewFloat64Schema().NewRef(),
+			}).NewRef(),
+		},
+		Serialization: map[string]interface{}{
+			"type":        "array",
+			"uniqueItems": true,
+			"items": map[string]interface{}{
+				"items": map[string]interface{}{
+					"type": "number",
+				},
+				"uniqueItems": true,
+				"type":        "array",
+			},
+		},
+		AllValid: []interface{}{
+			[]interface{}{
+				[]interface{}{1, 2},
+				[]interface{}{3, 4},
+			},
+			[]interface{}{ // Slice have items with the same value but with different index will treated as different slices
+				[]interface{}{1, 2},
+				[]interface{}{2, 1},
+			},
+		},
+		AllInvalid: []interface{}{
+			[]interface{}{ // Violate outer array uniqueItems: true
+				[]interface{}{8, 9},
+				[]interface{}{8, 9},
+			},
+			[]interface{}{ // Violate inner array uniqueItems: true
+				[]interface{}{9, 9},
+				[]interface{}{8, 8},
+			},
+		},
+	},
+
+	{
+		Title: "ARRAY : items format 'array' and array with object type items",
+		Schema: &openapi3.Schema{
+			Type:        "array",
+			UniqueItems: true,
+			Items: (&openapi3.Schema{
+				Type:        "array",
+				UniqueItems: true,
+				Items: (&openapi3.Schema{
+					Type: "object",
+					Properties: map[string]*openapi3.SchemaRef{
+						"key1": openapi3.NewFloat64Schema().NewRef(),
+					},
+				}).NewRef(),
+			}).NewRef(),
+		},
+		Serialization: map[string]interface{}{
+			"type":        "array",
+			"uniqueItems": true,
+			"items": map[string]interface{}{
+				"items": map[string]interface{}{
+					"type": "object",
+					"properties": map[string]interface{}{
+						"key1": map[string]interface{}{
+							"type": "number",
+						},
+					},
+				},
+				"uniqueItems": true,
+				"type":        "array",
+			},
+		},
+		AllValid: []interface{}{
+			[]interface{}{
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 1,
+					},
+				},
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 2,
+					},
+				},
+			},
+			[]interface{}{ // Slice have items with the same value but with different index will treated as different slices
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 1,
+					},
+					map[string]interface{}{
+						"key1": 2,
+					},
+				},
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 2,
+					},
+					map[string]interface{}{
+						"key1": 1,
+					},
+				},
+			},
+		},
+		AllInvalid: []interface{}{
+			[]interface{}{ // Violate outer array uniqueItems: true
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 1,
+					},
+					map[string]interface{}{
+						"key1": 2,
+					},
+				},
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 1,
+					},
+					map[string]interface{}{
+						"key1": 2,
+					},
+				},
+			},
+			[]interface{}{ // Violate inner array uniqueItems: true
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 1,
+					},
+					map[string]interface{}{
+						"key1": 1,
+					},
+				},
+				[]interface{}{
+					map[string]interface{}{
+						"key1": 2,
+					},
+					map[string]interface{}{
+						"key1": 2,
+					},
+				},
+			},
+		},
+	},
 
 	{
 		Title: "OBJECT",
@@ -727,3 +1017,31 @@ var schemaErrorExamples = []schemaErrorExample{
 		Want: "NEST",
 	},
 }
+
+func TestRegisterArrayUniqueItemsChecker(t *testing.T) {
+	var (
+		checker = func(items []interface{}) bool {
+			return false
+		}
+		scheme = openapi3.Schema{
+			Type:        "array",
+			UniqueItems: true,
+			Items:       openapi3.NewStringSchema().NewRef(),
+		}
+		val = []interface{}{"1", "2", "3"}
+		err error
+	)
+
+	// Fist checked by predefined function
+	err = scheme.VisitJSON(val)
+	require.NoError(t, err)
+
+	// Register a function will always return false when check if a
+	// slice has unique items, then use a slice indeed has unique
+	// items to verify that check unique items will failed.
+	openapi3.RegisterArrayUniqueItemsChecker(checker)
+
+	err = scheme.VisitJSON(val)
+	require.Error(t, err)
+	require.True(t, strings.HasPrefix(err.Error(), "Duplicate items found"))
+}