Repository: ogen-go/ogen
Pull request #398 (2022-06-15 13:28:45)

fix(jsonschema): validate enum values for uniqueness

ernado: please rebase

Patch:
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index 03614f04..9db51bed 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -6,6 +6,8 @@ import (
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
+
+	ogenjson "github.com/ogen-go/ogen/json"
 )
 
 // Num represents JSON number.
@@ -47,7 +49,7 @@ type RawSchema struct {
 	OneOf                []*RawSchema          `json:"oneOf,omitempty"`
 	AnyOf                []*RawSchema          `json:"anyOf,omitempty"`
 	Discriminator        *Discriminator        `json:"discriminator,omitempty"`
-	Enum                 []json.RawMessage     `json:"enum,omitempty"`
+	Enum                 Enum                  `json:"enum,omitempty"`
 	MultipleOf           Num                   `json:"multipleOf,omitempty"`
 	Maximum              Num                   `json:"maximum,omitempty"`
 	ExclusiveMaximum     bool                  `json:"exclusiveMaximum,omitempty"`
@@ -95,6 +97,27 @@ func (r *RawSchema) UnmarshalJSON(data []byte) error {
 	})
 }
 
+// Enum is JSON Schema enum validator description.
+type Enum []json.RawMessage
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (n *Enum) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	return d.Arr(func(d *jx.Decoder) error {
+		val, err := d.RawAppend(nil)
+		if err != nil {
+			return errors.Wrapf(err, "parse [%d]", len(*n))
+		}
+		for _, x := range *n {
+			if eq, _ := ogenjson.Equal(x, val); eq {
+				return errors.Errorf("value %q is duplicated", x)
+			}
+		}
+		*n = append(*n, json.RawMessage(val))
+		return nil
+	})
+}
+
 // RawProperty is item of RawProperties.
 type RawProperty struct {
 	Name   string
diff --git a/spec.go b/spec.go
index fa895fb8..c750a597 100644
--- a/spec.go
+++ b/spec.go
@@ -9,8 +9,12 @@ import (
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
-// Num represents JSON number.
-type Num = jsonschema.Num
+type (
+	// Num represents JSON number.
+	Num = jsonschema.Num
+	// Enum is JSON Schema enum validator description.
+	Enum = jsonschema.Enum
+)
 
 // Spec is the root document object of the OpenAPI document.
 type Spec struct {
@@ -387,7 +391,7 @@ type Schema struct {
 	// The value of this keyword MUST be an array.
 	// This array SHOULD have at least one element.
 	// Elements in the array SHOULD be unique.
-	Enum []json.RawMessage `json:"enum,omitempty"`
+	Enum Enum `json:"enum,omitempty"`
 
 	// The value of "multipleOf" MUST be a number, strictly greater than 0.
 	//


Test patch:
diff --git a/jsonschema/raw_schema_test.go b/jsonschema/raw_schema_test.go
new file mode 100644
index 00000000..758a060b
--- /dev/null
+++ b/jsonschema/raw_schema_test.go
@@ -0,0 +1,32 @@
+package jsonschema
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEnum_UnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		data    string
+		wantErr bool
+	}{
+		{`[{}, {}]`, true},
+		{`[1.0, 1.0]`, true},
+		{`[null, null]`, true},
+		{`[{}, {"a":"b"}]`, false},
+		{`[{"b":"a","a":"b"}, {"a":"b", "b":"a"}]`, true},
+	}
+	for i, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			var e Enum
+			if err := e.UnmarshalJSON([]byte(tt.data)); tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}