From 7413c27ab9eefcc373d894d98834c62e40bcd423 Mon Sep 17 00:00:00 2001 From: slessard Date: Wed, 14 Dec 2022 02:24:06 -0800 Subject: [PATCH] openapi3filter: Include schema ref or title in response body validation errors (#699) Co-authored-by: Steve Lessard --- openapi3filter/options_test.go | 2 +- openapi3filter/validate_request.go | 4 +++- openapi3filter/validate_response.go | 28 ++++++++++++++++++++++++- openapi3filter/validation_error_test.go | 12 +++++------ routers/gorillamux/example_test.go | 2 +- routers/legacy/validate_request_test.go | 2 +- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go index a95b6bb96..fd19329ff 100644 --- a/openapi3filter/options_test.go +++ b/openapi3filter/options_test.go @@ -78,5 +78,5 @@ paths: fmt.Println(err.Error()) - // Output: request body has an error: doesn't match the schema: field "Some field" must be an integer + // Output: request body has an error: doesn't match schema: field "Some field" must be an integer } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 8a747724e..2424eb9ed 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -286,10 +286,12 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + schemaId := getSchemaIdentifier(contentType.Schema) + schemaId = prependSpaceIfNeeded(schemaId) return &RequestError{ Input: input, RequestBody: requestBody, - Reason: "doesn't match the schema", + Reason: fmt.Sprintf("doesn't match schema%s", schemaId), Err: err, } } diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index abcbb4e9d..27bef82d3 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "sort" + "strings" "github.com/getkin/kin-openapi/openapi3" ) @@ -159,11 +160,36 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error // Validate data with the schema. if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil { + schemaId := getSchemaIdentifier(contentType.Schema) + schemaId = prependSpaceIfNeeded(schemaId) return &ResponseError{ Input: input, - Reason: "response body doesn't match the schema", + Reason: fmt.Sprintf("response body doesn't match schema%s", schemaId), Err: err, } } return nil } + +// getSchemaIdentifier gets something by which a schema could be identified. +// A schema by itself doesn't have a true identity field. This function makes +// a best effort to get a value that can fill that void. +func getSchemaIdentifier(schema *openapi3.SchemaRef) string { + var id string + + if schema != nil { + id = strings.TrimSpace(schema.Ref) + } + if id == "" && schema.Value != nil { + id = strings.TrimSpace(schema.Value.Title) + } + + return id +} + +func prependSpaceIfNeeded(value string) string { + if len(value) > 0 { + value = " " + value + } + return value +} diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index 6fee1355d..b84d8bdb6 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -322,7 +322,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", @@ -336,7 +336,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "photoUrls" is missing`, wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaPath: "/photoUrls", @@ -350,7 +350,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/name", @@ -364,7 +364,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/tags/0/name", @@ -378,7 +378,7 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", wantErrSchemaReason: "field must be set to array or not be present", wantErrSchemaPath: "/photoUrls", wantErrSchemaValue: "string", @@ -393,7 +393,7 @@ func getValidationTests(t *testing.T) []*validationTest { args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)), }, - wantErrReason: "doesn't match the schema", + wantErrReason: "doesn't match schema", wantErrSchemaPath: "/", wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginReason: `property "photoUrls" is missing`, diff --git a/routers/gorillamux/example_test.go b/routers/gorillamux/example_test.go index 2ca3225a5..54058cde2 100644 --- a/routers/gorillamux/example_test.go +++ b/routers/gorillamux/example_test.go @@ -53,7 +53,7 @@ func Example() { err = openapi3filter.ValidateResponse(ctx, responseValidationInput) fmt.Println(err) // Output: - // response body doesn't match the schema: field must be set to string or not be present + // response body doesn't match schema pathref.openapi.yml#/components/schemas/TestSchema: field must be set to string or not be present // Schema: // { // "type": "string" diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go index 5b9518c78..9c15ed44a 100644 --- a/routers/legacy/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -107,6 +107,6 @@ func Example() { fmt.Println(err) } // Output: - // request body has an error: doesn't match the schema: input matches more than one oneOf schemas + // request body has an error: doesn't match schema: input matches more than one oneOf schemas }