From 487700a9a352697e15a6251e211d7ce101eb816a Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Mon, 11 Jul 2022 12:34:37 +0100 Subject: [PATCH 01/10] add example support for floats and arrays --- reflect.go | 92 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/reflect.go b/reflect.go index 1b6732d..263b881 100644 --- a/reflect.go +++ b/reflect.go @@ -635,7 +635,7 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.Description = f.Tag.Get("jsonschema_description") tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema")) - t.genericKeywords(tags, parent, propertyName) + t.genericKeywords(tags, parent, propertyName, f) switch t.Type { case "string": @@ -646,15 +646,13 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.numbericKeywords(tags) case "array": t.arrayKeywords(tags) - case "boolean": - t.booleanKeywords(tags) } extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",") t.extraKeywords(extras) } // read struct tags for generic keyworks -func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) { +func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string, f reflect.StructField) { for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { @@ -705,24 +703,14 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str f, _ := strconv.ParseFloat(val, 64) t.Enum = append(t.Enum, f) } - } - } - } -} - -// read struct tags for boolean type keyworks -func (t *Schema) booleanKeywords(tags []string) { - for _, tag := range tags { - nameValue := strings.Split(tag, "=") - if len(nameValue) != 2 { - continue - } - name, val := nameValue[0], nameValue[1] - if name == "default" { - if val == "true" { - t.Default = true - } else if val == "false" { - t.Default = false + case "default": + if v, ok := parseValue(val, f.Type); ok { + t.Default = v + } + case "example": + if v, ok := parseValue(val, f.Type); ok { + t.Examples = append(t.Examples, v) + } } } } @@ -755,10 +743,6 @@ func (t *Schema) stringKeywords(tags []string) { case "writeOnly": i, _ := strconv.ParseBool(val) t.WriteOnly = i - case "default": - t.Default = val - case "example": - t.Examples = append(t.Examples, val) } } } @@ -786,13 +770,6 @@ func (t *Schema) numbericKeywords(tags []string) { case "exclusiveMinimum": b, _ := strconv.ParseBool(val) t.ExclusiveMinimum = b - case "default": - i, _ := strconv.Atoi(val) - t.Default = i - case "example": - if i, err := strconv.Atoi(val); err == nil { - t.Examples = append(t.Examples, i) - } } } } @@ -816,7 +793,6 @@ func (t *Schema) numbericKeywords(tags []string) { // read struct tags for array type keyworks func (t *Schema) arrayKeywords(tags []string) { - var defaultValues []interface{} for _, tag := range tags { nameValue := strings.Split(tag, "=") if len(nameValue) == 2 { @@ -830,8 +806,6 @@ func (t *Schema) arrayKeywords(tags []string) { t.MaxItems = i case "uniqueItems": t.UniqueItems = true - case "default": - defaultValues = append(defaultValues, val) case "enum": switch t.Items.Type { case "string": @@ -848,9 +822,6 @@ func (t *Schema) arrayKeywords(tags []string) { } } } - if len(defaultValues) > 0 { - t.Default = defaultValues - } } func (t *Schema) extraKeywords(tags []string) { @@ -1059,6 +1030,49 @@ func fullyQualifiedTypeName(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } +func parseValue(val string, t reflect.Type) (parsed interface{}, ok bool) { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i, err := strconv.Atoi(val) + return i, err == nil + + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(val, 64) + return f, err == nil + + case reflect.Bool: + if val == "true" { + return true, true + } else if val == "false" { + return false, true + } else { + return false, false + } + + case reflect.String: + return val, true + + case reflect.Pointer: + return parseValue(val, t.Elem()) + + case reflect.Slice, reflect.Array: + vals := strings.Split(val, ";") + parsed := make([]interface{}, len(vals)) + for i, v := range vals { + p, ok := parseValue(v, t.Elem()) + if !ok { + return nil, false + } + parsed[i] = p + } + return parsed, true + + default: + return nil, false + } +} + // AddGoComments will update the reflectors comment map with all the comments // found in the provided source directories. See the #ExtractGoComments method // for more details. From 47b7c09211727871a9a9d3f9069b7505abeb3ed8 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Mon, 11 Jul 2022 13:04:17 +0100 Subject: [PATCH 02/10] allow = in keyword value --- reflect.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/reflect.go b/reflect.go index 263b881..f906713 100644 --- a/reflect.go +++ b/reflect.go @@ -654,7 +654,7 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p // read struct tags for generic keyworks func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string, f reflect.StructField) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { @@ -719,7 +719,7 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str // read struct tags for string type keyworks func (t *Schema) stringKeywords(tags []string) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { @@ -751,7 +751,7 @@ func (t *Schema) stringKeywords(tags []string) { // read struct tags for numberic type keyworks func (t *Schema) numbericKeywords(tags []string) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { @@ -778,7 +778,7 @@ func (t *Schema) numbericKeywords(tags []string) { // read struct tags for object type keyworks // func (t *Type) objectKeywords(tags []string) { // for _, tag := range tags{ -// nameValue := strings.Split(tag, "=") +// nameValue := strings.SplitN(tag, "=", 2) // name, val := nameValue[0], nameValue[1] // switch name{ // case "dependencies": @@ -794,7 +794,7 @@ func (t *Schema) numbericKeywords(tags []string) { // read struct tags for array type keyworks func (t *Schema) arrayKeywords(tags []string) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { name, val := nameValue[0], nameValue[1] switch name { @@ -826,7 +826,7 @@ func (t *Schema) arrayKeywords(tags []string) { func (t *Schema) extraKeywords(tags []string) { for _, tag := range tags { - nameValue := strings.Split(tag, "=") + nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { t.setExtra(nameValue[0], nameValue[1]) } From 9134b86746247dade32597a5ca376d4eea196839 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Mon, 11 Jul 2022 13:06:43 +0100 Subject: [PATCH 03/10] use reflect.Ptr instead of reflect.Pointer --- reflect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflect.go b/reflect.go index f906713..f200b40 100644 --- a/reflect.go +++ b/reflect.go @@ -1053,7 +1053,7 @@ func parseValue(val string, t reflect.Type) (parsed interface{}, ok bool) { case reflect.String: return val, true - case reflect.Pointer: + case reflect.Ptr: return parseValue(val, t.Elem()) case reflect.Slice, reflect.Array: From 1c5b685b5422ed3d97a358c0402418d0242b6a40 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Mon, 11 Jul 2022 15:15:32 +0100 Subject: [PATCH 04/10] switch on Schema.Type instead of reflect.Type --- reflect.go | 95 +++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/reflect.go b/reflect.go index f200b40..41d34b8 100644 --- a/reflect.go +++ b/reflect.go @@ -635,7 +635,7 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.Description = f.Tag.Get("jsonschema_description") tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema")) - t.genericKeywords(tags, parent, propertyName, f) + t.genericKeywords(tags, parent, propertyName) switch t.Type { case "string": @@ -651,8 +651,52 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.extraKeywords(extras) } +func (t *Schema) parseValue(val string) (parsed interface{}, ok bool) { + switch t.Type { + case "number": + if i, err := strconv.Atoi(val); err == nil { + return i, true + } else if f, err := strconv.ParseFloat(val, 64); err == nil { + return f, true + } else { + return nil, false + } + + case "integer": + i, err := strconv.Atoi(val) + return i, err == nil + + case "boolean": + if val == "true" { + return true, true + } else if val == "false" { + return false, true + } else { + return false, false + } + + case "string": + return val, true + + case "array": + vals := strings.Split(val, ";") + parsed := make([]interface{}, len(vals)) + for i, v := range vals { + p, ok := t.Items.parseValue(v) + if !ok { + return nil, false + } + parsed[i] = p + } + return parsed, true + + default: + return nil, false + } +} + // read struct tags for generic keyworks -func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string, f reflect.StructField) { +func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) { for _, tag := range tags { nameValue := strings.SplitN(tag, "=", 2) if len(nameValue) == 2 { @@ -704,11 +748,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str t.Enum = append(t.Enum, f) } case "default": - if v, ok := parseValue(val, f.Type); ok { + if v, ok := t.parseValue(val); ok { t.Default = v } case "example": - if v, ok := parseValue(val, f.Type); ok { + if v, ok := t.parseValue(val); ok { t.Examples = append(t.Examples, v) } } @@ -1030,49 +1074,6 @@ func fullyQualifiedTypeName(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } -func parseValue(val string, t reflect.Type) (parsed interface{}, ok bool) { - switch t.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - i, err := strconv.Atoi(val) - return i, err == nil - - case reflect.Float32, reflect.Float64: - f, err := strconv.ParseFloat(val, 64) - return f, err == nil - - case reflect.Bool: - if val == "true" { - return true, true - } else if val == "false" { - return false, true - } else { - return false, false - } - - case reflect.String: - return val, true - - case reflect.Ptr: - return parseValue(val, t.Elem()) - - case reflect.Slice, reflect.Array: - vals := strings.Split(val, ";") - parsed := make([]interface{}, len(vals)) - for i, v := range vals { - p, ok := parseValue(v, t.Elem()) - if !ok { - return nil, false - } - parsed[i] = p - } - return parsed, true - - default: - return nil, false - } -} - // AddGoComments will update the reflectors comment map with all the comments // found in the provided source directories. See the #ExtractGoComments method // for more details. From adae5d29700e49d176768c0d031c73cf22c87b80 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Wed, 20 Jul 2022 13:20:00 +0100 Subject: [PATCH 05/10] allow examples for objects --- reflect.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/reflect.go b/reflect.go index 41d34b8..8bdc3e6 100644 --- a/reflect.go +++ b/reflect.go @@ -690,6 +690,13 @@ func (t *Schema) parseValue(val string) (parsed interface{}, ok bool) { } return parsed, true + case "", "object": + obj := make(map[string]interface{}) + if err := json.Unmarshal([]byte(val), &obj); err != nil { + return nil, false + } + return obj, true + default: return nil, false } From f98e7ac944e6321bc8027900776cb4fd691e6290 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman Date: Thu, 4 Aug 2022 12:20:26 +0100 Subject: [PATCH 06/10] add tests --- fixtures/examples.json | 103 +++++++++++++++++++++++++++++++++++++++++ reflect_test.go | 10 ++++ 2 files changed, 113 insertions(+) create mode 100644 fixtures/examples.json diff --git a/fixtures/examples.json b/fixtures/examples.json new file mode 100644 index 0000000..d9492bd --- /dev/null +++ b/fixtures/examples.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/invopop/jsonschema/examples", + "$ref": "#/$defs/Examples", + "$defs": { + "Examples": { + "properties": { + "string_example": { + "type": "string", + "examples": [ + "hi", + "test" + ] + }, + "int_example": { + "type": "integer", + "examples": [ + 1, + 10, + 42 + ] + }, + "float_example": { + "type": "number", + "examples": [ + 2, + 3.14, + 13.37 + ] + }, + "int_array_example": { + "items": { + "type": "integer" + }, + "type": "array", + "examples": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "map_example": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "examples": [ + { + "key": "value" + } + ] + }, + "map_array_example": { + "items": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array", + "examples": [ + [ + { + "a": "b" + }, + { + "c": "d" + } + ], + [ + { + "hello": "test" + } + ] + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "string_example", + "int_example", + "float_example", + "int_array_example", + "map_example", + "map_array_example" + ] + } + } +} \ No newline at end of file diff --git a/reflect_test.go b/reflect_test.go index f02a71a..28cea1e 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -289,6 +289,15 @@ type KeyNamed struct { RenamedByComputation int `jsonschema_description:"Description was preserved"` } +type Examples struct { + StringExample string `json:"string_example" jsonschema:"example=hi,example=test"` + IntExample int `json:"int_example" jsonschema:"example=1,example=10,example=42"` + FloatExample float64 `json:"float_example" jsonschema:"example=2.0,example=3.14,example=13.37"` + IntArrayExample []int `json:"int_array_example" jsonschema:"example=1;2,example=3;4,example=5;6"` + MapExample map[string]string `json:"map_example" jsonschema:"example={\"key\": \"value\"}"` + MapArrayExample []map[string]string `json:"map_array_example" jsonschema:"example={\"a\": \"b\"};{\"c\": \"d\"},example={\"hello\": \"test\"}"` +} + func TestReflector(t *testing.T) { r := new(Reflector) s := "http://example.com/schema" @@ -417,6 +426,7 @@ func TestSchemaGeneration(t *testing.T) { }, "fixtures/keynamed.json"}, {MapType{}, &Reflector{}, "fixtures/map_type.json"}, {ArrayType{}, &Reflector{}, "fixtures/array_type.json"}, + {Examples{}, &Reflector{}, "fixtures/examples.json"}, } for _, tt := range tests { From d7cd2ebb7f7af0dd84fc249948b0c6f2b1d301b4 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:44:14 +0000 Subject: [PATCH 07/10] simplify array enum parsing --- reflect.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/reflect.go b/reflect.go index 0410849..fe7105b 100644 --- a/reflect.go +++ b/reflect.go @@ -869,15 +869,8 @@ func (t *Schema) arrayKeywords(tags []string) { case "uniqueItems": t.UniqueItems = true case "enum": - switch t.Items.Type { - case "string": - t.Items.Enum = append(t.Items.Enum, val) - case "integer": - i, _ := strconv.Atoi(val) - t.Items.Enum = append(t.Items.Enum, i) - case "number": - f, _ := strconv.ParseFloat(val, 64) - t.Items.Enum = append(t.Items.Enum, f) + if v, ok := t.Items.parseValue(val); ok { + t.Items.Enum = append(t.Items.Enum, v) } case "format": t.Items.Format = val From 1d1197fb301eff8ccd3ecfa824f78ae9d475180c Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:01:56 +0000 Subject: [PATCH 08/10] add interface example --- fixtures/examples.json | 18 +++++++++++++++++- reflect.go | 11 +++++++++-- reflect_test.go | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/fixtures/examples.json b/fixtures/examples.json index 29a8f45..fc77143 100644 --- a/fixtures/examples.json +++ b/fixtures/examples.json @@ -82,6 +82,21 @@ } ] ] + }, + "any_example": { + "default": true, + "examples": [ + 1234, + "string_example", + { + "test": 42 + }, + [ + 1, + "str", + true + ] + ] } }, "additionalProperties": false, @@ -92,7 +107,8 @@ "float_example", "int_array_example", "map_example", - "map_array_example" + "map_array_example", + "any_example" ] } } diff --git a/reflect.go b/reflect.go index fe7105b..b47c81a 100644 --- a/reflect.go +++ b/reflect.go @@ -655,8 +655,15 @@ func (t *Schema) parseValue(val string) (parsed interface{}, ok bool) { } return parsed, true - case "", "object": - obj := make(map[string]interface{}) + case "object": + obj := make(map[string]any) + if err := json.Unmarshal([]byte(val), &obj); err != nil { + return nil, false + } + return obj, true + + case "": + var obj any if err := json.Unmarshal([]byte(val), &obj); err != nil { return nil, false } diff --git a/reflect_test.go b/reflect_test.go index 32aed9b..cd0a111 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -316,6 +316,7 @@ type Examples struct { IntArrayExample []int `json:"int_array_example" jsonschema:"example=1;2,example=3;4,example=5;6"` MapExample map[string]string `json:"map_example" jsonschema:"example={\"key\": \"value\"}"` MapArrayExample []map[string]string `json:"map_array_example" jsonschema:"example={\"a\": \"b\"};{\"c\": \"d\"},example={\"hello\": \"test\"}"` + AnyExample any `json:"any_example" jsonschema:"example=1234,example=\"string_example\",example={\"test\": 42},example=[1\\,\"str\"\\,true],default=true"` } type SchemaExtendTestBase struct { From 88790752c4f56faff5e38266b1c5878f8b45f938 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:07:04 +0000 Subject: [PATCH 09/10] add doc comment --- reflect.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reflect.go b/reflect.go index b47c81a..c2bd67e 100644 --- a/reflect.go +++ b/reflect.go @@ -628,7 +628,11 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.extraKeywords(extras) } -func (t *Schema) parseValue(val string) (parsed interface{}, ok bool) { +// parseValue parses a string into a value matching the type of this schema. +// It is used to parse default and example values from a struct tag. +// If the string could be successfully parsed into the target type, +// the second return value will be set to true. +func (t *Schema) parseValue(val string) (any, bool) { switch t.Type { case "number": return toJSONNumber(val) From f65a7e0b8d835f0c77df75990b606915c7b8f7d6 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:08:24 +0000 Subject: [PATCH 10/10] fix use of interface{} --- reflect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflect.go b/reflect.go index c2bd67e..7f11659 100644 --- a/reflect.go +++ b/reflect.go @@ -649,7 +649,7 @@ func (t *Schema) parseValue(val string) (any, bool) { case "array": vals := strings.Split(val, ";") - parsed := make([]interface{}, len(vals)) + parsed := make([]any, len(vals)) for i, v := range vals { p, ok := t.Items.parseValue(v) if !ok {