From e61fbc6186a20547036f06212e2c94a89aa68a7c Mon Sep 17 00:00:00 2001 From: shadowpigy Date: Mon, 2 Sep 2024 00:53:27 +0800 Subject: [PATCH 1/2] fix jsonschema tests --- jsonschema/json_test.go | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 744706082..224443dc6 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -17,7 +17,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { { name: "Test with empty Definition", def: jsonschema.Definition{}, - want: `{"properties":{}}`, + want: `{}`, }, { name: "Test with Definition properties set", @@ -35,8 +35,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { "description":"A string type", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" } } }`, @@ -66,12 +65,10 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" } } } @@ -114,23 +111,19 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" }, "address":{ "type":"object", "properties":{ "city":{ - "type":"string", - "properties":{} + "type":"string" }, "country":{ - "type":"string", - "properties":{} + "type":"string" } } } @@ -146,25 +139,11 @@ func TestDefinition_MarshalJSON(t *testing.T) { Items: &jsonschema.Definition{ Type: jsonschema.String, }, - Properties: map[string]jsonschema.Definition{ - "name": { - Type: jsonschema.String, - }, - }, }, want: `{ "type":"array", "items":{ - "type":"string", - "properties":{ - - } - }, - "properties":{ - "name":{ - "type":"string", - "properties":{} - } + "type":"string" } }`, }, From b807bf8f34424751d536b72362399b969fed74a7 Mon Sep 17 00:00:00 2001 From: shadowpigy Date: Fri, 6 Sep 2024 03:31:23 +0800 Subject: [PATCH 2/2] ordered jsonschema def --- jsonschema/json.go | 44 ++++++--- jsonschema/json_test.go | 175 +++++++++++++++--------------------- jsonschema/validate.go | 2 +- jsonschema/validate_test.go | 48 ++++++---- 4 files changed, 138 insertions(+), 131 deletions(-) diff --git a/jsonschema/json.go b/jsonschema/json.go index bcb253fae..9cdcf8976 100644 --- a/jsonschema/json.go +++ b/jsonschema/json.go @@ -24,6 +24,12 @@ const ( Boolean DataType = "boolean" ) +// OrderedProperties represents an ordered map of property names to their definitions +type OrderedProperties struct { + Keys []string + Values map[string]Definition +} + // Definition is a struct for describing a JSON Schema. // It is fairly limited, and you may have better luck using a third-party library. type Definition struct { @@ -35,7 +41,7 @@ type Definition struct { // one element, where each element is unique. You will probably only use this with strings. Enum []string `json:"enum,omitempty"` // Properties describes the properties of an object, if the schema type is Object. - Properties map[string]Definition `json:"properties,omitempty"` + Properties OrderedProperties `json:"-"` // Required specifies which properties are required, if the schema type is Object. Required []string `json:"required,omitempty"` // Items specifies which data type an array contains, if the schema type is Array. @@ -49,15 +55,31 @@ type Definition struct { } func (d *Definition) MarshalJSON() ([]byte, error) { - if d.Properties == nil { - d.Properties = make(map[string]Definition) - } type Alias Definition - return json.Marshal(struct { - Alias + aux := struct { + *Alias + Properties json.RawMessage `json:"properties,omitempty"` }{ - Alias: (Alias)(*d), - }) + Alias: (*Alias)(d), + } + + if len(d.Properties.Keys) > 0 { + orderedProps := make(map[string]json.RawMessage) + for _, key := range d.Properties.Keys { + value, err := json.Marshal(d.Properties.Values[key]) + if err != nil { + return nil, err + } + orderedProps[key] = value + } + var err error + aux.Properties, err = json.Marshal(orderedProps) + if err != nil { + return nil, err + } + } + + return json.Marshal(aux) } func (d *Definition) Unmarshal(content string, v any) error { @@ -114,8 +136,8 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { var d = Definition{ Type: Object, AdditionalProperties: false, + Properties: OrderedProperties{Keys: make([]string, 0), Values: make(map[string]Definition)}, } - properties := make(map[string]Definition) var requiredFields []string for i := 0; i < t.NumField(); i++ { field := t.Field(i) @@ -139,7 +161,8 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { if description != "" { item.Description = description } - properties[jsonTag] = *item + d.Properties.Keys = append(d.Properties.Keys, jsonTag) + d.Properties.Values[jsonTag] = *item if s := field.Tag.Get("required"); s != "" { required, _ = strconv.ParseBool(s) @@ -149,6 +172,5 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { } } d.Required = requiredFields - d.Properties = properties return &d, nil } diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 224443dc6..4f6965a57 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -11,53 +11,25 @@ import ( func TestDefinition_MarshalJSON(t *testing.T) { tests := []struct { name string - def jsonschema.Definition + def any want string }{ { name: "Test with empty Definition", - def: jsonschema.Definition{}, - want: `{}`, - }, - { - name: "Test with Definition properties set", - def: jsonschema.Definition{ - Type: jsonschema.String, - Description: "A string type", - Properties: map[string]jsonschema.Definition{ - "name": { - Type: jsonschema.String, - }, - }, - }, + def: struct{}{}, want: `{ - "type":"string", - "description":"A string type", - "properties":{ - "name":{ - "type":"string" - } - } + "type":"object", + "additionalProperties":false }`, }, { name: "Test with nested Definition properties", - def: jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "user": { - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "name": { - Type: jsonschema.String, - }, - "age": { - Type: jsonschema.Integer, - }, - }, - }, - }, - }, + def: struct { + User struct { + Name string `json:"name"` + Age int `json:"age"` + } `json:"user"` + }{}, want: `{ "type":"object", "properties":{ @@ -72,38 +44,23 @@ func TestDefinition_MarshalJSON(t *testing.T) { } } } - } + }, + "additionalProperties":false, + "required":["user"] }`, }, { name: "Test with complex nested Definition", - def: jsonschema.Definition{ - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "user": { - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "name": { - Type: jsonschema.String, - }, - "age": { - Type: jsonschema.Integer, - }, - "address": { - Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "city": { - Type: jsonschema.String, - }, - "country": { - Type: jsonschema.String, - }, - }, - }, - }, - }, - }, - }, + def: struct { + User struct { + Name string `json:"name"` + Age int `json:"age"` + Address struct { + City string `json:"city"` + Country string `json:"country"` + } `json:"address"` + } `json:"user"` + }{}, want: `{ "type":"object", "properties":{ @@ -127,19 +84,18 @@ func TestDefinition_MarshalJSON(t *testing.T) { } } } - } + }, + "additionalProperties":false, + "required":["name","age","address"] } - } + }, + "additionalProperties":false, + "required":["user"] }`, }, { name: "Test with Array type Definition", - def: jsonschema.Definition{ - Type: jsonschema.Array, - Items: &jsonschema.Definition{ - Type: jsonschema.String, - }, - }, + def: []string{}, want: `{ "type":"array", "items":{ @@ -147,44 +103,61 @@ func TestDefinition_MarshalJSON(t *testing.T) { } }`, }, + { + name: "Test order prevention", + def: struct { + C string `json:"c"` + A int `json:"a"` + B bool `json:"b"` + }{}, + want: `{ + "type":"object", + "properties":{ + "c":{ + "type":"string" + }, + "a":{ + "type":"integer" + }, + "b":{ + "type":"boolean" + } + }, + "additionalProperties":false, + "required":["c","a","b"] +} +`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { wantBytes := []byte(tt.want) - var want map[string]interface{} - err := json.Unmarshal(wantBytes, &want) + var wantDef jsonschema.Definition + err := json.Unmarshal(wantBytes, &wantDef) if err != nil { t.Errorf("Failed to Unmarshal JSON: error = %v", err) return } - - got := structToMap(t, tt.def) - gotPtr := structToMap(t, &tt.def) - - if !reflect.DeepEqual(got, want) { - t.Errorf("MarshalJSON() got = %v, want %v", got, want) + schema, err := jsonschema.GenerateSchemaForType(tt.def) + if err != nil { + t.Errorf("Failed to Unmarshal JSON: error = %v", err) + return } - if !reflect.DeepEqual(gotPtr, want) { - t.Errorf("MarshalJSON() gotPtr = %v, want %v", gotPtr, want) + gotBytes, err := schema.MarshalJSON() + if err != nil { + t.Errorf("Failed to Marshal JSON: error = %v", err) + return + } + var gotDef jsonschema.Definition + err = json.Unmarshal(gotBytes, &gotDef) + if err != nil { + t.Errorf("Failed to Unmarshal JSON: error = %v", err) + return + } + if !reflect.DeepEqual(wantDef, gotDef) { + t.Errorf("Definition.MarshalJSON() = %v, want %v", gotDef, wantDef) } }) } } - -func structToMap(t *testing.T, v any) map[string]any { - t.Helper() - gotBytes, err := json.Marshal(v) - if err != nil { - t.Errorf("Failed to Marshal JSON: error = %v", err) - return nil - } - - var got map[string]interface{} - err = json.Unmarshal(gotBytes, &got) - if err != nil { - t.Errorf("Failed to Unmarshal JSON: error = %v", err) - return nil - } - return got -} diff --git a/jsonschema/validate.go b/jsonschema/validate.go index f14ffd4c4..491b1ee87 100644 --- a/jsonschema/validate.go +++ b/jsonschema/validate.go @@ -55,7 +55,7 @@ func validateObject(schema Definition, data any) bool { return false } } - for key, valueSchema := range schema.Properties { + for key, valueSchema := range schema.Properties.Values { value, exists := dataMap[key] if exists && !Validate(valueSchema, value) { return false diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go index c2c47a2ce..7a2b3f451 100644 --- a/jsonschema/validate_test.go +++ b/jsonschema/validate_test.go @@ -47,12 +47,15 @@ func Test_Validate(t *testing.T) { "number": 123.4, "boolean": false, "array": []any{1, 2, 3}, - }, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ - "string": {Type: jsonschema.String}, - "integer": {Type: jsonschema.Integer}, - "number": {Type: jsonschema.Number}, - "boolean": {Type: jsonschema.Boolean}, - "array": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}}, + }, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: jsonschema.OrderedProperties{ + Keys: []string{"string", "integer", "number", "boolean", "array"}, + Values: map[string]jsonschema.Definition{ + "string": {Type: jsonschema.String}, + "integer": {Type: jsonschema.Integer}, + "number": {Type: jsonschema.Number}, + "boolean": {Type: jsonschema.Boolean}, + "array": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}}, + }, }, Required: []string{"string"}, }}, true}, @@ -61,12 +64,15 @@ func Test_Validate(t *testing.T) { "number": 123.4, "boolean": false, "array": []any{1, 2, 3}, - }, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{ - "string": {Type: jsonschema.String}, - "integer": {Type: jsonschema.Integer}, - "number": {Type: jsonschema.Number}, - "boolean": {Type: jsonschema.Boolean}, - "array": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}}, + }, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: jsonschema.OrderedProperties{ + Keys: []string{"string", "integer", "number", "boolean", "array"}, + Values: map[string]jsonschema.Definition{ + "string": {Type: jsonschema.String}, + "integer": {Type: jsonschema.Integer}, + "number": {Type: jsonschema.Number}, + "boolean": {Type: jsonschema.Boolean}, + "array": {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}}, + }, }, Required: []string{"string"}, }}, false}, @@ -102,9 +108,12 @@ func TestUnmarshal(t *testing.T) { {"", args{ schema: jsonschema.Definition{ Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "string": {Type: jsonschema.String}, - "number": {Type: jsonschema.Number}, + Properties: jsonschema.OrderedProperties{ + Keys: []string{"string", "number"}, + Values: map[string]jsonschema.Definition{ + "string": {Type: jsonschema.String}, + "number": {Type: jsonschema.Number}, + }, }, }, content: []byte(`{"string":"abc","number":123.4}`), @@ -113,9 +122,12 @@ func TestUnmarshal(t *testing.T) { {"", args{ schema: jsonschema.Definition{ Type: jsonschema.Object, - Properties: map[string]jsonschema.Definition{ - "string": {Type: jsonschema.String}, - "number": {Type: jsonschema.Number}, + Properties: jsonschema.OrderedProperties{ + Keys: []string{"string", "number"}, + Values: map[string]jsonschema.Definition{ + "string": {Type: jsonschema.String}, + "number": {Type: jsonschema.Number}, + }, }, Required: []string{"string", "number"}, },