diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..00a9078d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Working memory and debug artifacts +working-memory/ \ No newline at end of file diff --git a/errors/validation_error.go b/errors/validation_error.go index 4c590aed..020f8bd2 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -9,29 +9,25 @@ import ( "github.com/santhosh-tekuri/jsonschema/v6" ) -// SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more -// user-friendly way to break down what went wrong. +// SchemaValidationFailure describes any failure that occurs when validating data +// against either an OpenAPI or JSON Schema. It aims to be a more user-friendly +// representation of the error than what is provided by the jsonschema library. type SchemaValidationFailure struct { // Reason is a human-readable message describing the reason for the error. Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` - // Location is the XPath-like location of the validation failure - Location string `json:"location,omitempty" yaml:"location,omitempty"` + // InstancePath is the raw path segments from the root to the failing field + InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"` // FieldName is the name of the specific field that failed validation (last segment of the path) FieldName string `json:"fieldName,omitempty" yaml:"fieldName,omitempty"` - // FieldPath is the JSONPath representation of the field location (e.g., "$.user.email") + // FieldPath is the JSONPath representation of the field location that failed validation (e.g., "$.user.email") FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"` - // InstancePath is the raw path segments from the root to the failing field - InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"` - - // DeepLocation is the path to the validation failure as exposed by the jsonschema library. - DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"` - - // AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library. - AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"` + // KeywordLocation is the JSON Pointer (RFC 6901) path to the schema keyword that failed validation + // (e.g., "/properties/age/minimum") + KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"` // Line is the line number where the violation occurred. This may a local line number // if the validation is a schema (only schemas are validated locally, so the line number will be relative to @@ -46,14 +42,18 @@ type SchemaValidationFailure struct { // ReferenceSchema is the schema that was referenced in the validation failure. ReferenceSchema string `json:"referenceSchema,omitempty" yaml:"referenceSchema,omitempty"` - // ReferenceObject is the object that was referenced in the validation failure. + // ReferenceObject is the object that failed schema validation ReferenceObject string `json:"referenceObject,omitempty" yaml:"referenceObject,omitempty"` // ReferenceExample is an example object generated from the schema that was referenced in the validation failure. ReferenceExample string `json:"referenceExample,omitempty" yaml:"referenceExample,omitempty"` - // The original error object, which is a jsonschema.ValidationError object. - OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"` + // The original jsonschema.ValidationError object, if the schema failure originated from the jsonschema library. + OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"` + + // DEPRECATED in favor of explicit use of FieldPath & InstancePath + // Location is the XPath-like location of the validation failure + Location string `json:"location,omitempty" yaml:"location,omitempty"` } // Error returns a string representation of the error @@ -97,7 +97,7 @@ type ValidationError struct { ParameterName string `json:"parameterName,omitempty" yaml:"parameterName,omitempty"` // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors - // This is only populated whe the validation type is against a schema. + // This is only populated when the validation type is against a schema. SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"` // Context is the object that the validation error occurred on. This is usually a pointer to a schema diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index bdd8034e..2885369b 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -127,23 +127,17 @@ func ValidateParameterSchema( jsch, err := helpers.NewCompiledSchema(name, jsonSchema, validationOptions) if err != nil { // schema compilation failed, return validation error instead of panicking - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: string(jsonSchema), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' failed schema compilation", entity, name), Reason: fmt.Sprintf("%s '%s' schema compilation failed: %s", reasonEntity, name, err.Error()), - SpecLine: 1, - SpecCol: 0, - ParameterName: name, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: string(jsonSchema), + SpecLine: 1, + SpecCol: 0, + ParameterName: name, + HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: string(jsonSchema), }) return validationErrors } @@ -226,12 +220,13 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val } fail := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - OriginalError: scErrs, + Reason: errMsg, + Location: er.KeywordLocation, // DEPRECATED + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + OriginalJsonSchemaError: scErrs, } if schema != nil { rendered, err := schema.RenderInline() diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index 6deaaf4d..c6e2e986 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -1308,8 +1308,8 @@ components: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Nil(t, errors[0].SchemaValidationErrors) + assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_SchemaNoType_Issue75(t *testing.T) { diff --git a/requests/validate_request.go b/requests/validate_request.go index 8ec74e1e..cb89624b 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -87,22 +87,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V input.Version, ) if err != nil { - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed schema compilation", input.Request.Method, input.Request.URL.Path), - Reason: fmt.Sprintf("The request schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + Reason: fmt.Sprintf("The request schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: referenceSchema, }) return false, validationErrors } @@ -138,23 +132,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V err := json.Unmarshal(requestBody, &decodedObj) if err != nil { // cannot decode the request body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(requestBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: errors.HowToFixInvalidSchema, + Context: referenceSchema, // attach the rendered schema to the error }) return false, validationErrors } @@ -171,22 +158,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V } // cannot decode the request body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: "request body is empty, but there is a schema defined", - ReferenceSchema: referenceSchema, - ReferenceObject: string(requestBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body is empty for '%s'", request.Method, request.URL.Path), - Reason: "The request body is empty but there is a schema defined", - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: "The request body is empty but there is a schema defined", + SpecLine: line, + SpecCol: col, + HowToFix: errors.HowToFixInvalidSchema, + Context: referenceSchema, // attach the rendered schema to the error }) return false, validationErrors } @@ -236,14 +217,15 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) violation := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - ReferenceSchema: referenceSchema, - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + Location: er.InstanceLocation, // DEPRECATED + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: referenceSchema, + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 5096225d..9e7e75e0 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -1262,7 +1262,8 @@ paths: assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Nil(t, errors[0].SchemaValidationErrors) + assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_NoContentType_Valid(t *testing.T) { diff --git a/responses/validate_response.go b/responses/validate_response.go index 91f64fb8..b6633e64 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -90,11 +90,6 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors input.Version, ) if err != nil { - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, @@ -102,11 +97,10 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors input.Response.StatusCode, input.Request.URL.Path), Reason: fmt.Sprintf("The response schema for status code '%d' failed to compile: %s", input.Response.StatusCode, err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: referenceSchema, }) return false, validationErrors } @@ -129,22 +123,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors if response == nil || response.Body == http.NoBody { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: "response is empty", - Location: "unavailable", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: "response", ValidationSubType: "object", Message: fmt.Sprintf("%s response object is missing for '%s'", request.Method, request.URL.Path), - Reason: "The response object is completely missing", - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "ensure response object has been set", - Context: referenceSchema, // attach the rendered schema to the error + Reason: "The response object is completely missing", + SpecLine: 1, + SpecCol: 0, + HowToFix: "ensure response object has been set", + Context: referenceSchema, // attach the rendered schema to the error }) return false, validationErrors } @@ -152,23 +140,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors responseBody, ioErr := io.ReadAll(response.Body) if ioErr != nil { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: ioErr.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(responseBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' cannot be read, it's empty or malformed", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The response body cannot be decoded: %s", ioErr.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "ensure body is not empty", - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The response body cannot be decoded: %s", ioErr.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "ensure body is not empty", + Context: referenceSchema, // attach the rendered schema to the error }) return false, validationErrors } @@ -183,23 +164,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors err := json.Unmarshal(responseBody, &decodedObj) if err != nil { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(responseBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' failed to validate schema", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: errors.HowToFixInvalidSchema, + Context: referenceSchema, // attach the rendered schema to the error }) return false, validationErrors } @@ -251,14 +225,15 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors } violation := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - ReferenceSchema: referenceSchema, - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + Location: er.InstanceLocation, // DEPRECATED + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: referenceSchema, + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 42d198ed..9282bd5e 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -40,21 +40,15 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo jsch, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) if err != nil { // schema compilation failed, return validation error instead of panicking - violation := &liberrors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile OpenAPI schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: loadedSchema, - } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: "schema", - ValidationSubType: "compilation", - Message: "OpenAPI document schema compilation failed", - Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: loadedSchema, + ValidationType: "schema", + ValidationSubType: "compilation", + Message: "OpenAPI document schema compilation failed", + Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: loadedSchema, }) return false, validationErrors } @@ -83,16 +77,15 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo // locate the violated property in the schema located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation) - violation := &liberrors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.InstanceLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - DeepLocation: er.KeywordLocation, - AbsoluteLocation: er.AbsoluteKeywordLocation, - OriginalError: jk, - } + violation := &liberrors.SchemaValidationFailure{ + Reason: errMsg, + Location: er.InstanceLocation, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + OriginalJsonSchemaError: jk, + } // if we have a location within the schema, add it to the error if located != nil { diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index eff930f8..b6104f62 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -132,8 +132,8 @@ func TestValidateDocument_CompilationFailure(t *testing.T) { valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "failed to compile OpenAPI schema") + assert.Contains(t, errors[0].Reason, "The OpenAPI schema failed to compile") + assert.Nil(t, errors[0].SchemaValidationErrors, "Compilation errors should not have SchemaValidationErrors") } func TestValidateSchema_ValidateLicenseIdentifier(t *testing.T) { diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 4d1c67a1..b2def790 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -133,22 +133,15 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload renderedSchema, e = schema.RenderInline() if e != nil { // schema cannot be rendered, so it's not valid! - violation := &liberrors.SchemaValidationFailure{ - Reason: e.Error(), - Location: "unavailable", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: "schema does not pass validation", - Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()), - SpecLine: schema.GoLow().GetRootNode().Line, - SpecCol: schema.GoLow().GetRootNode().Column, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "schema does not pass validation", + Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()), + SpecLine: schema.GoLow().GetRootNode().Line, + SpecCol: schema.GoLow().GetRootNode().Column, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) s.lock.Unlock() return false, validationErrors @@ -163,12 +156,6 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload if err != nil { // cannot decode the request body, so it's not valid - violation := &liberrors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { @@ -176,15 +163,14 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: "schema does not pass validation", - Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "schema does not pass validation", + Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), + SpecLine: line, + SpecCol: col, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) return false, validationErrors } @@ -199,12 +185,6 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload var schemaValidationErrors []*liberrors.SchemaValidationFailure if err != nil { - violation := &liberrors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "schema compilation", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { @@ -212,15 +192,14 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.Schema, - ValidationSubType: helpers.Schema, - Message: "schema compilation failed", - Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()), - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.Schema, + ValidationSubType: helpers.Schema, + Message: "schema compilation failed", + Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()), + SpecLine: line, + SpecCol: col, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) return false, validationErrors } @@ -300,16 +279,15 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, } violation := &liberrors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.InstanceLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - DeepLocation: er.KeywordLocation, - AbsoluteLocation: er.AbsoluteKeywordLocation, - ReferenceSchema: string(renderedSchema), - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + Location: er.InstanceLocation, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: string(renderedSchema), + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { diff --git a/schema_validation/validate_schema_openapi_test.go b/schema_validation/validate_schema_openapi_test.go index 7ab3d328..54c01ea9 100644 --- a/schema_validation/validate_schema_openapi_test.go +++ b/schema_validation/validate_schema_openapi_test.go @@ -345,13 +345,13 @@ components: foundCompilationError := false for _, err := range errors { - if err.SchemaValidationErrors != nil { - for _, schErr := range err.SchemaValidationErrors { - if schErr.Location == "unavailable" && schErr.Reason == "schema render failure, circular reference: `#/components/schemas/b`" { - foundCompilationError = true - } - } + if err.Message == "schema does not pass validation" && + err.Reason != "" && + (err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/b`" || + err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/Node`") { + foundCompilationError = true } + assert.Nil(t, err.SchemaValidationErrors, "Rendering errors should not have SchemaValidationErrors") } assert.True(t, foundCompilationError, "Should have schema compilation error for circular references") }) diff --git a/schema_validation/validate_schema_test.go b/schema_validation/validate_schema_test.go index 7c7229fd..7bf63036 100644 --- a/schema_validation/validate_schema_test.go +++ b/schema_validation/validate_schema_test.go @@ -524,8 +524,52 @@ paths: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Equal(t, "schema does not pass validation", errors[0].Message) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Contains(t, errors[0].Reason, "invalid character '}' looking for beginning of object key string") + assert.Nil(t, errors[0].SchemaValidationErrors) +} + +func TestValidateSchema_CompilationFailure(t *testing.T) { + // Test that schema compilation failure doesn't create SchemaValidationErrors + // This uses an extremely complex regex that might fail compilation in some regex engines + spec := `openapi: 3.1.0 +paths: + /test: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + password: + type: string + pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}(?:(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,})*'` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + sch := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema + + // create a schema validator with strict regex engine that might fail on complex patterns + v := NewSchemaValidator() + + // Try to validate - if compilation fails, we should get an error without SchemaValidationErrors + validData := `{"password": "ValidPass123!"}` + valid, errors := v.ValidateSchemaString(sch.Schema(), validData) + + // This test is environment-dependent - compilation might succeed or fail depending on regex engine + // If it fails, we want to ensure SchemaValidationErrors is nil + if !valid && len(errors) > 0 { + for _, err := range errors { + if err.Message == "schema compilation failed" { + // Compilation failure should NOT have SchemaValidationErrors + assert.Nil(t, err.SchemaValidationErrors, "Schema compilation errors should not have SchemaValidationErrors") + t.Logf("Schema compilation failed as expected: %s", err.Reason) + } + } + } else { + t.Skip("Regex engine handled the complex pattern - skipping compilation failure test") + } } //// https://github.com/pb33f/libopenapi-validator/issues/26