Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add example support for floats and arrays #31

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
115 changes: 115 additions & 0 deletions fixtures/examples.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
109 changes: 65 additions & 44 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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":
Expand All @@ -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 {
Expand All @@ -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":
Expand All @@ -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
Expand All @@ -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)
}
}

Expand Down
11 changes: 11 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"},
Expand Down