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

openapi3: fix schema validation for anyOf, allOf, oneOf operations #850

Merged
merged 2 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions openapi3/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,7 +1059,11 @@ func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOptio
func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) {
switch value := value.(type) {
case nil:
return schema.visitJSONNull(settings)
// Don't use VisitJSONNull, as we still want to reach 'visitXOFOperations', since
// those could allow for a nullable value even though this one doesn't
if schema.Nullable {
return
}
case float64:
if math.IsNaN(value) {
return ErrSchemaInputNaN
Expand All @@ -1070,13 +1074,28 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
}

if schema.IsEmpty() {
switch value.(type) {
case nil:
return schema.visitJSONNull(settings)
default:
return
}
}

if err = schema.visitNotOperation(settings, value); err != nil {
return
}
if err = schema.visitSetOperations(settings, value); err != nil {
var run bool
if err, run = schema.visitXOFOperations(settings, value); err != nil || !run {
return
}
if err = schema.visitEnumOperation(settings, value); err != nil {
return
}

switch value := value.(type) {
case nil:
return schema.visitJSONNull(settings)
case bool:
return schema.visitJSONBoolean(settings, value)
case json.Number:
Expand Down Expand Up @@ -1137,7 +1156,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
}
}

func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) {
func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, value interface{}) (err error) {
if enum := schema.Enum; len(enum) != 0 {
for _, v := range enum {
switch c := value.(type) {
Expand Down Expand Up @@ -1171,7 +1190,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
customizeMessageError: settings.customizeMessageError,
}
}
return
}

func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, value interface{}) (err error) {
if ref := schema.Not; ref != nil {
v := ref.Value
if v == nil {
Expand All @@ -1189,7 +1211,13 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
}
}
return
}

// If the XOF operations pass successfully, abort further run of validation, as they will already be satisfied (unless the schema
// itself is badly specified
func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value interface{}) (err error, run bool) {
var visitedOneOf, visitedAnyOf, visitedAllOf bool
if v := schema.OneOf; len(v) > 0 {
var discriminatorRef string
if schema.Discriminator != nil {
Expand All @@ -1201,7 +1229,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
}
}, false
}

discriminatorValString, okcheck := discriminatorVal.(string)
Expand All @@ -1211,7 +1239,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
}
}, false
}

if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
Expand All @@ -1220,7 +1248,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
Schema: schema,
SchemaField: "discriminator",
Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
}
}, false
}
}
}
Expand All @@ -1234,7 +1262,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
for idx, item := range v {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
return foundUnresolvedRef(item.Ref), false
}

if discriminatorRef != "" && discriminatorRef != item.Ref {
Expand All @@ -1257,7 +1285,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val

if ok != 1 {
if settings.failfast {
return errSchema
return errSchema, false
}
e := &SchemaError{
Value: value,
Expand All @@ -1273,13 +1301,14 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
e.Reason = `value doesn't match any schema from "oneOf"`
}

return e
return e, false
}

// run again to inject default value that defined in matched oneOf schema
if settings.asreq || settings.asrep {
_ = v[matchedOneOfIndices[0]].Value.visitJSON(settings, value)
}
visitedOneOf = true
}

if v := schema.AnyOf; len(v) > 0 {
Expand All @@ -1291,7 +1320,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
for idx, item := range v {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
return foundUnresolvedRef(item.Ref), false
}
// make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
if settings.asreq || settings.asrep {
Expand All @@ -1305,28 +1334,29 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
if !ok {
if settings.failfast {
return errSchema
return errSchema, false
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "anyOf",
Reason: `doesn't match any schema from "anyOf"`,
customizeMessageError: settings.customizeMessageError,
}
}, false
}

_ = v[matchedAnyOfIdx].Value.visitJSON(settings, value)
visitedAnyOf = true
}

for _, item := range schema.AllOf {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
return foundUnresolvedRef(item.Ref), false
}
if err := v.visitJSON(settings, value); err != nil {
if settings.failfast {
return errSchema
return errSchema, false
}
return &SchemaError{
Value: value,
Expand All @@ -1335,9 +1365,12 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
Reason: `doesn't match all schemas from "allOf"`,
Origin: err,
customizeMessageError: settings.customizeMessageError,
}
}, false
}
visitedAllOf = true
}

run = !(visitedOneOf || visitedAnyOf || visitedAllOf)
return
}

Expand Down
25 changes: 25 additions & 0 deletions openapi3/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,31 @@ var schemaExamples = []schemaExample{
},
},

{
Title: "ANYOF NULLABLE CHILD",
Schema: NewAnyOfSchema(
NewIntegerSchema().WithNullable(),
NewFloat64Schema(),
),
Serialization: map[string]interface{}{
"anyOf": []interface{}{
map[string]interface{}{"type": "integer", "nullable": true},
map[string]interface{}{"type": "number"},
},
},
AllValid: []interface{}{
nil,
42,
4.2,
},
AllInvalid: []interface{}{
true,
[]interface{}{42},
"bla",
map[string]interface{}{},
},
},

{
Title: "BOOLEAN",
Schema: NewBoolSchema(),
Expand Down
Loading