diff --git a/fixtures/examples.json b/fixtures/examples.json new file mode 100644 index 0000000..fc77143 --- /dev/null +++ b/fixtures/examples.json @@ -0,0 +1,115 @@ +{ + "$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": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "examples": [ + { + "key": "value" + } + ] + }, + "map_array_example": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "array", + "examples": [ + [ + { + "a": "b" + }, + { + "c": "d" + } + ], + [ + { + "hello": "test" + } + ] + ] + }, + "any_example": { + "default": true, + "examples": [ + 1234, + "string_example", + { + "test": 42 + }, + [ + 1, + "str", + true + ] + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "string_example", + "int_example", + "float_example", + "int_array_example", + "map_example", + "map_array_example", + "any_example" + ] + } + } +} \ No newline at end of file diff --git a/reflect.go b/reflect.go index 3249c8c..7f11659 100644 --- a/reflect.go +++ b/reflect.go @@ -623,13 +623,61 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p t.numericalKeywords(tags) case "array": t.arrayKeywords(tags) - case "boolean": - t.booleanKeywords(tags) } extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",") t.extraKeywords(extras) } +// 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) + + case "integer": + i, err := strconv.Atoi(val) + return i, err == nil + + case "boolean": + return val == "true", val == "true" || val == "false" + + case "string": + return val, true + + case "array": + vals := strings.Split(val, ";") + parsed := make([]any, len(vals)) + for i, v := range vals { + p, ok := t.Items.parseValue(v) + if !ok { + return nil, false + } + parsed[i] = p + } + return parsed, true + + 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 + } + return obj, true + + default: + return nil, false + } +} + // read struct tags for generic keywords func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) []string { //nolint:gocyclo unprocessed := make([]string, 0, len(tags)) @@ -728,6 +776,14 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str Type: ty, }) } + case "default": + if v, ok := t.parseValue(val); ok { + t.Default = v + } + case "example": + if v, ok := t.parseValue(val); ok { + t.Examples = append(t.Examples, v) + } default: unprocessed = append(unprocessed, tag) } @@ -736,24 +792,6 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str return unprocessed } -// read struct tags for boolean type keywords -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 - } - } - } -} - // read struct tags for string type keywords func (t *Schema) stringKeywords(tags []string) { for _, tag := range tags { @@ -778,10 +816,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) case "enum": t.Enum = append(t.Enum, val) } @@ -792,7 +826,7 @@ func (t *Schema) stringKeywords(tags []string) { // read struct tags for numerical type keywords func (t *Schema) numericalKeywords(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 { @@ -806,14 +840,6 @@ func (t *Schema) numericalKeywords(tags []string) { t.ExclusiveMaximum, _ = toJSONNumber(val) case "exclusiveMinimum": t.ExclusiveMinimum, _ = toJSONNumber(val) - case "default": - if num, ok := toJSONNumber(val); ok { - t.Default = num - } - case "example": - if num, ok := toJSONNumber(val); ok { - t.Examples = append(t.Examples, num) - } case "enum": if num, ok := toJSONNumber(val); ok { t.Enum = append(t.Enum, num) @@ -826,7 +852,7 @@ func (t *Schema) numericalKeywords(tags []string) { // read struct tags for object type keywords // 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": @@ -841,11 +867,9 @@ func (t *Schema) numericalKeywords(tags []string) { // read struct tags for array type keywords func (t *Schema) arrayKeywords(tags []string) { - var defaultValues []any - unprocessed := make([]string, 0, len(tags)) 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 { @@ -855,8 +879,10 @@ func (t *Schema) arrayKeywords(tags []string) { t.MaxItems = parseUint(val) case "uniqueItems": t.UniqueItems = true - case "default": - defaultValues = append(defaultValues, val) + case "enum": + if v, ok := t.Items.parseValue(val); ok { + t.Items.Enum = append(t.Items.Enum, v) + } case "format": t.Items.Format = val case "pattern": @@ -866,9 +892,6 @@ func (t *Schema) arrayKeywords(tags []string) { } } } - if len(defaultValues) > 0 { - t.Default = defaultValues - } if len(unprocessed) == 0 { // we don't have anything else to process @@ -884,8 +907,6 @@ func (t *Schema) arrayKeywords(tags []string) { t.Items.numericalKeywords(unprocessed) case "array": // explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong - case "boolean": - t.Items.booleanKeywords(unprocessed) } } diff --git a/reflect_test.go b/reflect_test.go index 94b6018..cd0a111 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -309,6 +309,16 @@ 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\"}"` + AnyExample any `json:"any_example" jsonschema:"example=1234,example=\"string_example\",example={\"test\": 42},example=[1\\,\"str\"\\,true],default=true"` +} + type SchemaExtendTestBase struct { FirstName string `json:"FirstName"` LastName string `json:"LastName"` @@ -467,6 +477,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"}, {SchemaExtendTest{}, &Reflector{}, "fixtures/custom_type_extend.json"}, {Expression{}, &Reflector{}, "fixtures/schema_with_expression.json"}, {PatternEqualsTest{}, &Reflector{}, "fixtures/equals_in_pattern.json"},