Skip to content

Commit e00a340

Browse files
authored
openapi3: preserve all validation errors for allOf (#1087)
Previously, only the first error would be reported
1 parent 6acf92b commit e00a340

File tree

5 files changed

+75
-19
lines changed

5 files changed

+75
-19
lines changed

openapi3/errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,13 @@ func (meo multiErrorForOneOf) Error() string {
5757
func (meo multiErrorForOneOf) Unwrap() error {
5858
return MultiError(meo)
5959
}
60+
61+
type multiErrorForAllOf MultiError
62+
63+
func (mea multiErrorForAllOf) Error() string {
64+
return spliceErr(" And ", mea)
65+
}
66+
67+
func (mea multiErrorForAllOf) Unwrap() error {
68+
return MultiError(mea)
69+
}

openapi3/schema.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val
14291429
visitedAnyOf = true
14301430
}
14311431

1432+
validationErrors := multiErrorForAllOf{}
14321433
for _, item := range schema.AllOf {
14331434
v := item.Value
14341435
if v == nil {
@@ -1438,17 +1439,20 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val
14381439
if settings.failfast {
14391440
return errSchema, false
14401441
}
1441-
return &SchemaError{
1442-
Value: value,
1443-
Schema: schema,
1444-
SchemaField: "allOf",
1445-
Reason: `doesn't match all schemas from "allOf"`,
1446-
Origin: err,
1447-
customizeMessageError: settings.customizeMessageError,
1448-
}, false
1442+
validationErrors = append(validationErrors, err)
14491443
}
14501444
visitedAllOf = true
14511445
}
1446+
if len(validationErrors) > 0 {
1447+
return &SchemaError{
1448+
Value: value,
1449+
Schema: schema,
1450+
SchemaField: "allOf",
1451+
Reason: `doesn't match all schemas from "allOf"`,
1452+
Origin: fmt.Errorf("doesn't match schema due to: %w", validationErrors),
1453+
customizeMessageError: settings.customizeMessageError,
1454+
}, false
1455+
}
14521456

14531457
run = !((visitedOneOf || visitedAnyOf || visitedAllOf) && value == nil)
14541458
return

openapi3filter/issue641_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ paths:
6969
name: "failed allof pattern",
7070
spec: allOfSpec,
7171
req: `/items?test=999999`,
72-
errStr: `parameter "test" in query has an error: string doesn't match the regular expression "^[0-9]{1,4}$"`,
72+
errStr: `parameter "test" in query has an error: doesn't match schema due to: string doesn't match the regular expression "^[0-9]{1,4}$"`,
7373
},
7474
}
7575

openapi3filter/issue789_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ paths:
6969
name: "failed allof object array",
7070
spec: allOfArraySpec,
7171
req: `/items?test=foo`,
72-
errStr: `parameter "test" in query has an error: string doesn't match the regular expression`,
72+
errStr: `parameter "test" in query has an error: doesn't match schema due to: string doesn't match the regular expression`,
7373
},
7474
{
7575
name: "success oneof string pattern match",

openapi3filter/validation_error_test.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openapi3filter
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"io"
89
"net/http"
@@ -47,6 +48,9 @@ type validationTest struct {
4748
wantErrSchemaOriginReason string
4849
wantErrSchemaOriginPath string
4950
wantErrSchemaOriginValue any
51+
wantMultiErrSchemaReasons []string
52+
wantMultiErrSchemaPaths []string
53+
wantMultiErrSchemaValues []any
5054
wantErrParam string
5155
wantErrParamIn string
5256
wantErrParseKind ParseErrorKind
@@ -478,17 +482,38 @@ func getValidationTests(t *testing.T) []*validationTest {
478482
args: validationArgs{
479483
r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)),
480484
},
481-
wantErrReason: "doesn't match schema",
482-
wantErrSchemaPath: "/",
483-
wantErrSchemaValue: map[string]string{"name": "Bahama"},
484-
wantErrSchemaReason: `doesn't match all schemas from "allOf"`,
485-
wantErrSchemaOriginReason: `property "photoUrls" is missing`,
486-
wantErrSchemaOriginValue: map[string]string{"name": "Bahama"},
487-
wantErrSchemaOriginPath: "/photoUrls",
485+
wantErrReason: "doesn't match schema",
486+
wantErrSchemaPath: "/",
487+
wantErrSchemaValue: map[string]string{"name": "Bahama"},
488+
wantErrSchemaReason: `doesn't match all schemas from "allOf"`,
489+
wantMultiErrSchemaPaths: []string{"/photoUrls"},
490+
wantMultiErrSchemaValues: []any{map[string]string{"name": "Bahama"}},
491+
wantMultiErrSchemaReasons: []string{
492+
`property "photoUrls" is missing`,
493+
},
488494
wantErrResponse: &ValidationError{
489495
Status: http.StatusUnprocessableEntity,
490-
Title: `property "photoUrls" is missing`,
491-
Source: &ValidationErrorSource{Pointer: "/photoUrls"},
496+
Title: `doesn't match all schemas from "allOf"`,
497+
},
498+
},
499+
{
500+
name: "error - missing required object attribute and bad type from allOf required overlay",
501+
args: validationArgs{
502+
r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":1}`)),
503+
},
504+
wantErrReason: "doesn't match schema",
505+
wantErrSchemaPath: "/",
506+
wantErrSchemaValue: map[string]float64{"name": 1},
507+
wantErrSchemaReason: `doesn't match all schemas from "allOf"`,
508+
wantMultiErrSchemaPaths: []string{"/name", "/photoUrls"},
509+
wantMultiErrSchemaValues: []any{1, map[string]float64{"name": 1}},
510+
wantMultiErrSchemaReasons: []string{
511+
"value must be a string",
512+
"property \"photoUrls\" is missing",
513+
},
514+
wantErrResponse: &ValidationError{
515+
Status: http.StatusUnprocessableEntity,
516+
Title: `doesn't match all schemas from "allOf"`,
492517
},
493518
},
494519
{
@@ -599,6 +624,23 @@ func TestValidationHandler_validateRequest(t *testing.T) {
599624
pointer := toJSONPointer(originErr.JSONPointer())
600625
req.Equal(tt.wantErrSchemaOriginPath, pointer)
601626
req.Equal(fmt.Sprint(tt.wantErrSchemaOriginValue), fmt.Sprint(originErr.Value))
627+
} else if wrapErr := errors.Unwrap(innerErr.Origin); wrapErr != nil {
628+
if multiErr, ok := errors.Unwrap(wrapErr).(openapi3.MultiError); ok {
629+
req.Len(multiErr, len(tt.wantMultiErrSchemaReasons))
630+
req.Len(multiErr, len(tt.wantMultiErrSchemaPaths))
631+
req.Len(multiErr, len(tt.wantMultiErrSchemaValues))
632+
for i, merr := range multiErr {
633+
schemaErr, ok := merr.(*openapi3.SchemaError)
634+
if !ok {
635+
continue
636+
}
637+
req.Equal(tt.wantMultiErrSchemaReasons[i], schemaErr.Reason)
638+
pointer := toJSONPointer(schemaErr.JSONPointer())
639+
req.Equal(tt.wantMultiErrSchemaPaths[i], pointer)
640+
req.Equal(fmt.Sprint(tt.wantMultiErrSchemaValues[i]), fmt.Sprint(schemaErr.Value))
641+
642+
}
643+
}
602644
}
603645
} else {
604646
req.False(tt.wantErrSchemaReason != "" || tt.wantErrSchemaPath != "",

0 commit comments

Comments
 (0)