-
Notifications
You must be signed in to change notification settings - Fork 94
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
Adds support for multi-types invopop/jsonschema#134 #140
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"$id": "https://github.com/invopop/jsonschema/multi-type-test", | ||
"$ref": "#/$defs/MultiTypeTest", | ||
"$defs": { | ||
"MultiTypeTest": { | ||
"properties": { | ||
"Value": { | ||
"type": [ | ||
"number", | ||
"object" | ||
] | ||
} | ||
}, | ||
"additionalProperties": false, | ||
"type": "object", | ||
"required": [ | ||
"Value" | ||
] | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ package jsonschema | |
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"net" | ||
"net/url" | ||
"reflect" | ||
|
@@ -295,8 +296,8 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) | |
// It will unmarshal either. | ||
if t.Implements(protoEnumType) { | ||
st.OneOf = []*Schema{ | ||
{Type: "string"}, | ||
{Type: "integer"}, | ||
{Type: &Type{Types: []string{"string"}}}, | ||
{Type: &Type{Types: []string{"integer"}}}, | ||
} | ||
return st | ||
} | ||
|
@@ -306,7 +307,7 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) | |
// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7 | ||
if t == ipType { | ||
// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5 | ||
st.Type = "string" | ||
st.Type = &Type{Types: []string{"string"}} | ||
st.Format = "ipv4" | ||
return st | ||
} | ||
|
@@ -326,16 +327,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) | |
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, | ||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | ||
st.Type = "integer" | ||
st.Type = &Type{Types: []string{"integer"}} | ||
|
||
case reflect.Float32, reflect.Float64: | ||
st.Type = "number" | ||
st.Type = &Type{Types: []string{"number"}} | ||
|
||
case reflect.Bool: | ||
st.Type = "boolean" | ||
st.Type = &Type{Types: []string{"boolean"}} | ||
|
||
case reflect.String: | ||
st.Type = "string" | ||
st.Type = &Type{Types: []string{"string"}} | ||
|
||
default: | ||
panic("unsupported type " + t.String()) | ||
|
@@ -400,19 +401,19 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, | |
st.MaxItems = &l | ||
} | ||
if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() { | ||
st.Type = "string" | ||
st.Type = &Type{Types: []string{"string"}} | ||
// NOTE: ContentMediaType is not set here | ||
st.ContentEncoding = "base64" | ||
} else { | ||
st.Type = "array" | ||
st.Type = &Type{Types: []string{"array"}} | ||
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem()) | ||
} | ||
} | ||
|
||
func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) { | ||
r.addDefinition(definitions, t, st) | ||
|
||
st.Type = "object" | ||
st.Type = &Type{Types: []string{"object"}} | ||
if st.Description == "" { | ||
st.Description = r.lookupComment(t, "") | ||
} | ||
|
@@ -435,17 +436,17 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc | |
// Handle special types | ||
switch t { | ||
case timeType: // date-time RFC section 7.3.1 | ||
s.Type = "string" | ||
s.Type = &Type{Types: []string{"string"}} | ||
s.Format = "date-time" | ||
return | ||
case uriType: // uri RFC section 7.3.6 | ||
s.Type = "string" | ||
s.Type = &Type{Types: []string{"string"}} | ||
s.Format = "uri" | ||
return | ||
} | ||
|
||
r.addDefinition(definitions, t, s) | ||
s.Type = "object" | ||
s.Type = &Type{Types: []string{"object"}} | ||
s.Properties = NewProperties() | ||
s.Description = r.lookupComment(t, "") | ||
if r.AssignAnchor { | ||
|
@@ -524,7 +525,7 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r | |
OneOf: []*Schema{ | ||
property, | ||
{ | ||
Type: "null", | ||
Type: &Type{Types: []string{"null"}}, | ||
}, | ||
}, | ||
} | ||
|
@@ -614,18 +615,23 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p | |
tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema")) | ||
tags = t.genericKeywords(tags, parent, propertyName) | ||
|
||
switch t.Type { | ||
case "string": | ||
t.stringKeywords(tags) | ||
case "number": | ||
t.numericalKeywords(tags) | ||
case "integer": | ||
t.numericalKeywords(tags) | ||
case "array": | ||
t.arrayKeywords(tags) | ||
case "boolean": | ||
t.booleanKeywords(tags) | ||
if t.Type != nil { | ||
for _, currType := range t.Type.Types { | ||
switch currType { | ||
case "string": | ||
t.stringKeywords(tags) | ||
case "number": | ||
t.numericalKeywords(tags) | ||
case "integer": | ||
t.numericalKeywords(tags) | ||
case "array": | ||
t.arrayKeywords(tags) | ||
case "boolean": | ||
t.booleanKeywords(tags) | ||
} | ||
} | ||
} | ||
|
||
extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",") | ||
t.extraKeywords(extras) | ||
} | ||
|
@@ -643,7 +649,8 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str | |
case "description": | ||
t.Description = val | ||
case "type": | ||
t.Type = val | ||
types := strings.Split(val, ";") | ||
t.Type = &Type{Types: types} | ||
case "anchor": | ||
t.Anchor = val | ||
case "oneof_required": | ||
|
@@ -695,11 +702,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str | |
if t.OneOf == nil { | ||
t.OneOf = make([]*Schema, 0, 1) | ||
} | ||
t.Type = "" | ||
t.Type = nil | ||
types := strings.Split(nameValue[1], ";") | ||
for _, ty := range types { | ||
t.OneOf = append(t.OneOf, &Schema{ | ||
Type: ty, | ||
Type: &Type{Types: []string{ty}}, | ||
}) | ||
} | ||
case "anyof_ref": | ||
|
@@ -721,11 +728,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str | |
if t.AnyOf == nil { | ||
t.AnyOf = make([]*Schema, 0, 1) | ||
} | ||
t.Type = "" | ||
t.Type = nil | ||
types := strings.Split(nameValue[1], ";") | ||
for _, ty := range types { | ||
t.AnyOf = append(t.AnyOf, &Schema{ | ||
Type: ty, | ||
Type: &Type{Types: []string{ty}}, | ||
}) | ||
} | ||
default: | ||
|
@@ -872,17 +879,23 @@ func (t *Schema) arrayKeywords(tags []string) { | |
return | ||
} | ||
|
||
switch t.Items.Type { | ||
case "string": | ||
t.Items.stringKeywords(unprocessed) | ||
case "number": | ||
t.Items.numericalKeywords(unprocessed) | ||
case "integer": | ||
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) | ||
if t.Items.Type == nil { | ||
return | ||
} | ||
|
||
for _, currType := range t.Items.Type.Types { | ||
switch currType { | ||
case "string": | ||
t.Items.stringKeywords(unprocessed) | ||
case "number": | ||
t.Items.numericalKeywords(unprocessed) | ||
case "integer": | ||
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) | ||
} | ||
} | ||
} | ||
|
||
|
@@ -1112,6 +1125,33 @@ func (t *Schema) MarshalJSON() ([]byte, error) { | |
return append(b, m[1:]...), nil | ||
} | ||
|
||
// MarshalJSON implements json.Marshaler | ||
func (tp *Type) MarshalJSON() ([]byte, error) { | ||
switch len(tp.Types) { | ||
case 0: | ||
return []byte("[]"), nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct? It should be tested either way. |
||
case 1: | ||
return json.Marshal(tp.Types[0]) | ||
default: | ||
return json.Marshal(tp.Types) | ||
} | ||
} | ||
|
||
// UnmarshalJSON implements json.Unm\arshaler | ||
func (tp *Type) UnmarshalJSON(data []byte) error { | ||
err := json.Unmarshal(data, &tp.Types) | ||
if err == nil { | ||
return nil | ||
} | ||
var v string | ||
err2 := json.Unmarshal(data, &v) | ||
if err2 != nil { | ||
return fmt.Errorf("could not read type into slice: %v, nor into string: %w", err.Error(), err2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't see any tests for this. |
||
} | ||
tp.Types = []string{v} | ||
return nil | ||
} | ||
|
||
func (r *Reflector) typeName(t reflect.Type) string { | ||
if r.Namer != nil { | ||
if name := r.Namer(t); name != "" { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -40,7 +40,7 @@ type Schema struct { | |||||||||
AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3 | ||||||||||
PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4 | ||||||||||
// RFC draft-bhutton-json-schema-validation-00, section 6 | ||||||||||
Type string `json:"type,omitempty"` // section 6.1.1 | ||||||||||
Type *Type `json:"type,omitempty"` // section 6.1.1 | ||||||||||
Enum []any `json:"enum,omitempty"` // section 6.1.2 | ||||||||||
Const any `json:"const,omitempty"` // section 6.1.3 | ||||||||||
MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1 | ||||||||||
|
@@ -92,3 +92,8 @@ var ( | |||||||||
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 | ||||||||||
// RFC draft-wright-json-schema-validation-00, section 5.26 | ||||||||||
type Definitions map[string]*Schema | ||||||||||
|
||||||||||
// Type implements section 6.1.1 | ||||||||||
type Type struct { | ||||||||||
Types []string | ||||||||||
} | ||||||||||
Comment on lines
+97
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you already consider this?
Suggested change
I'd also suggest adding helper methods to avoid all the messy assignments, instead of:
I'd expect:
With a method pattern that looks like:
(I'm using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd define these methods alongside the
Type
struct in theschema.go
file.