diff --git a/README.md b/README.md index 27d6699cf..b85af9864 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,74 @@ func arrayUniqueItemsChecker(items []interface{}) bool { } ``` +## Custom function to change schema error messages + +By default, the error message returned when validating a value includes the error reason, the schema, and the input value. + +For example, given the following schema: + +```json +{ + "type": "string", + "allOf": [ + { "pattern": "[A-Z]" }, + { "pattern": "[a-z]" }, + { "pattern": "[0-9]" }, + { "pattern": "[!@#$%^&*()_+=-?~]" } + ] +} +``` + +Passing the input value `"secret"` to this schema will produce the following error message: + +``` +string doesn't match the regular expression "[A-Z]" +Schema: + { + "pattern": "[A-Z]" + } + +Value: + "secret" +``` + +Including the original value in the error message can be helpful for debugging, but it may not be appropriate for sensitive information such as secrets. + +To disable the extra details in the schema error message, you can set the `openapi3.SchemaErrorDetailsDisabled` option to `true`: + +```go +func main() { + // ... + + // Disable schema error detailed error messages + openapi3.SchemaErrorDetailsDisabled = true + + // ... other validate codes +} +``` + +This will shorten the error message to present only the reason: + +``` +string doesn't match the regular expression "[A-Z]" +``` + +For more fine-grained control over the error message, you can pass a custom `openapi3filter.Options` object to `openapi3filter.RequestValidationInput` that includes a `openapi3filter.CustomSchemaErrorFunc`. + +```go +func validationOptions() *openapi3filter.Options { + options := openapi3filter.DefaultOptions + options.WithCustomSchemaErrorFunc(safeErrorMessage) + return options +} + +func safeErrorMessage(err *openapi3.SchemaError) string { + return err.Reason +} +``` + +This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value. + ## Sub-v0 breaking API changes ### v0.113.0 diff --git a/openapi3/schema.go b/openapi3/schema.go index 9d21c00e6..df76c00e4 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1125,7 +1125,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: value, Schema: schema, SchemaField: "enum", - Reason: fmt.Sprintf("value %q is not one of the allowed values", value), + Reason: fmt.Sprintf("value is not one of the allowed values %q", schema.Enum), customizeMessageError: settings.customizeMessageError, } } @@ -1164,16 +1164,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val discriminatorValString, okcheck := discriminatorVal.(string) if !okcheck { - valStr := "null" - if discriminatorVal != nil { - valStr = fmt.Sprintf("%v", discriminatorVal) - } - return &SchemaError{ Value: discriminatorVal, Schema: schema, SchemaField: "discriminator", - Reason: fmt.Sprintf("value of discriminator property %q is not a string: %v", pn, valStr), + Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn), } } @@ -1182,7 +1177,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val Value: discriminatorVal, Schema: schema, SchemaField: "discriminator", - Reason: fmt.Sprintf("discriminator property %q has invalid value: %q", pn, discriminatorVal), + Reason: fmt.Sprintf("discriminator property %q has invalid value", pn), } } } @@ -1351,7 +1346,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "type", - Reason: fmt.Sprintf("value \"%g\" must be an integer", value), + Reason: fmt.Sprintf("value must be an integer"), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1571,7 +1566,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value Value: value, Schema: schema, SchemaField: "pattern", - Reason: fmt.Sprintf(`string %q doesn't match the regular expression "%s"`, value, schema.Pattern), + Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern), customizeMessageError: settings.customizeMessageError, } if !settings.multiError { @@ -1932,10 +1927,12 @@ func (schema *Schema) compilePattern() (err error) { } type SchemaError struct { - Value interface{} - reversePath []string - Schema *Schema - SchemaField string + Value interface{} + reversePath []string + Schema *Schema + SchemaField string + // Reason is a human-readable message describing the error. + // The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages. Reason string Origin error customizeMessageError func(err *SchemaError) string diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 1a8ea8138..90a23cc98 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -96,7 +96,7 @@ func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { "name": "snoopy", "$type": "snake", }) - require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value: \"snake\"") + require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value") } func TestVisitJSON_OneOf_MissingField(t *testing.T) { @@ -126,14 +126,14 @@ func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) { "scratches": true, "$type": 1, }) - require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: 1") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string") err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", "barks": true, "$type": nil, }) - require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string: null") + require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string") } func TestVisitJSON_OneOf_Path(t *testing.T) { diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go index 7e2eaabe1..ec0b2a1f1 100644 --- a/openapi3filter/issue201_test.go +++ b/openapi3filter/issue201_test.go @@ -98,7 +98,7 @@ paths: }, "invalid required header": { - err: `response header "X-Blup" doesn't match schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`, + err: `response header "X-Blup" doesn't match schema: string doesn't match the regular expression "^blup$"`, headers: map[string]string{ "X-Blip": "blip", "x-blop": "blop", diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go index 9a2964284..ee0be1019 100644 --- a/openapi3filter/issue641_test.go +++ b/openapi3filter/issue641_test.go @@ -69,7 +69,7 @@ paths: name: "failed allof pattern", spec: allOfSpec, req: `/items?test=999999`, - errStr: `parameter "test" in query has an error: string "999999" doesn't match the regular expression "^[0-9]{1,4}$"`, + errStr: `parameter "test" in query has an error: string doesn't match the regular expression "^[0-9]{1,4}$"`, }, } diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 0ee48ad39..9fb7cfefd 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -93,7 +93,7 @@ func Example() { // // ===== Start New Error ===== // @body.status: - // Error at "/status": value "invalidStatus" is not one of the allowed values + // Error at "/status": value is not one of the allowed values ["available" "pending" "sold"] // Schema: // { // "description": "pet status in the store", diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index a27556f77..bc2730064 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -244,11 +244,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value \"available,sold\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"available,sold\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? @@ -262,11 +262,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "status", wantErrParamIn: "query", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, @@ -278,11 +278,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "kind", wantErrParamIn: "query", - wantErrSchemaReason: "value \"fish,with,commas\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"fish,with,commas\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"dog\" \"cat\" \"turtle\" \"bird,with,commas\"]", Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, @@ -304,11 +304,11 @@ func getValidationTests(t *testing.T) []*validationTest { }, wantErrParam: "x-environment", wantErrParamIn: "header", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"demo\" \"prod\"]", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"demo\" \"prod\"]", Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, @@ -323,11 +323,11 @@ func getValidationTests(t *testing.T) []*validationTest { r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired", - wantErrSchemaReason: "value \"watdis\" is not one of the allowed values", + wantErrSchemaReason: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, - Title: "value \"watdis\" is not one of the allowed values", + Title: "value is not one of the allowed values [\"available\" \"pending\" \"sold\"]", Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, },