From b87cec988431749ae47bdb5092a4687ffa3862d2 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 8 Oct 2025 18:08:07 -0700 Subject: [PATCH 1/9] Refactor SchemaValidationFailure struct fields --- errors/validation_error.go | 35 +++++++++++++------------- parameters/validate_parameter.go | 12 ++++----- requests/validate_request.go | 16 ++++++------ responses/validate_response.go | 16 ++++++------ schema_validation/validate_document.go | 16 ++++++------ schema_validation/validate_schema.go | 20 +++++++-------- 6 files changed, 57 insertions(+), 58 deletions(-) diff --git a/errors/validation_error.go b/errors/validation_error.go index 4c590aed..80904ec6 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -9,29 +9,27 @@ 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"` + // KeywordLocation is the relative path to the JsonSchema keyword that failed validation + KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"` - // AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library. - AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"` + // AbsoluteKeywordLocation is the absolute path to the validation failure as exposed by the jsonschema library. + AbsoluteKeywordLocation string `json:"absoluteKeywordLocation,omitempty" yaml:"absoluteKeywordLocation,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 +44,15 @@ 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 jsonschema.ValidationError object, if the schema failure originated from the jsonschema library. + OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"` - // The original error object, which is a jsonschema.ValidationError object. - OriginalError *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 +96,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 12dbd83d..d9914caa 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -226,12 +226,12 @@ 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, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + OriginalJsonSchemaError: scErrs, } if schema != nil { rendered, err := schema.RenderInline() diff --git a/requests/validate_request.go b/requests/validate_request.go index 64550cba..f6f6aac8 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -167,14 +167,14 @@ func ValidateRequestSchema( 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: string(renderedSchema), - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + Location: er.KeywordLocation, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + 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/responses/validate_response.go b/responses/validate_response.go index 70115337..865ddb5b 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -194,14 +194,14 @@ func ValidateResponseSchema( } violation := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - ReferenceSchema: string(renderedSchema), - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + Location: er.KeywordLocation, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + 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_document.go b/schema_validation/validate_document.go index 42d198ed..d17cf0e4 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -84,14 +84,14 @@ 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, + Reason: errMsg, + Location: er.InstanceLocation, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 4d1c67a1..f20b92b5 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -300,16 +300,16 @@ 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, + AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, + ReferenceSchema: string(renderedSchema), + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { From 56c1f23a35d274dd91c5ded19eb04549f0fee5ab Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 10 Oct 2025 11:55:31 -0700 Subject: [PATCH 2/9] document expectation if SchemaValidationFailure did not originate from JSON Schema --- errors/validation_error.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/errors/validation_error.go b/errors/validation_error.go index 80904ec6..f695e684 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -26,9 +26,11 @@ type SchemaValidationFailure struct { FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"` // KeywordLocation is the relative path to the JsonSchema keyword that failed validation + // This will be empty if the validation failure did not originate from JSON Schema validation KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"` // AbsoluteKeywordLocation is the absolute path to the validation failure as exposed by the jsonschema library. + // This will be empty if the validation failure did not originate from JSON Schema validation AbsoluteKeywordLocation string `json:"absoluteKeywordLocation,omitempty" yaml:"absoluteKeywordLocation,omitempty"` // Line is the line number where the violation occurred. This may a local line number From 5e582420f13215f8090e7f46706d3de11a05e366 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 15 Oct 2025 10:36:44 -0700 Subject: [PATCH 3/9] add ReferenceExample back --- errors/validation_error.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/errors/validation_error.go b/errors/validation_error.go index f695e684..5c83a6dc 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -49,6 +49,9 @@ type SchemaValidationFailure struct { // 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 jsonschema.ValidationError object, if the schema failure originated from the jsonschema library. OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"` From 6c03ca51c1591cee2a9282c69c5fd210b6cfd419 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 5 Nov 2025 17:11:13 -0800 Subject: [PATCH 4/9] Remove SchemaValidationFailure from schema pre-validation errors --- schema_validation/validate_schema.go | 69 +++++++------------ .../validate_schema_openapi_test.go | 12 ++-- schema_validation/validate_schema_test.go | 48 ++++++++++++- 3 files changed, 76 insertions(+), 53 deletions(-) diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index f20b92b5..427f0520 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 } 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 From dbc56eb21d9d6c6319828aa199b9af6f6348d613 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 5 Nov 2025 17:27:01 -0800 Subject: [PATCH 5/9] Remove SchemaValidationFailure from document compilation errors --- schema_validation/validate_document.go | 22 ++++++++------------- schema_validation/validate_document_test.go | 4 ++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index d17cf0e4..788f5d60 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 } 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) { From f0fc5b5b72f88ee3c566e2ae848f71cef4ca699f Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 5 Nov 2025 19:05:43 -0800 Subject: [PATCH 6/9] Parameters: add KeywordLocation when formatting JSON schema errors, remove SchemaValidationFailure from when json schema compilation fails --- parameters/validate_parameter.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index fa43069e..2f5d7d31 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 } @@ -227,10 +221,12 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val fail := &errors.SchemaValidationFailure{ Reason: errMsg, - Location: er.KeywordLocation, + Location: er.KeywordLocation, // DEPRECATED FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, OriginalJsonSchemaError: scErrs, } if schema != nil { From 3e80f7597f3bd994b8af4df33f6d5ccc2400fa2a Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 7 Nov 2025 15:19:46 -0800 Subject: [PATCH 7/9] Remove AbsoluteKeywordLocation field - never populated due to schema inlining AbsoluteKeywordLocation was never populated because libopenapi's RenderInline() method resolves and inlines all $ref references before schemas reach the JSON Schema validator. Since the validator never encounters $ref pointers, this field remained empty in all cases and served no purpose. Removed from: - SchemaValidationFailure struct definition - All instantiation sites (schema_validation, parameters, requests) - Improved KeywordLocation documentation with JSON Pointer reference --- .gitignore | 2 ++ errors/validation_error.go | 8 ++------ parameters/validate_parameter.go | 1 - schema_validation/validate_document.go | 19 +++++++++---------- schema_validation/validate_schema.go | 1 - 5 files changed, 13 insertions(+), 18 deletions(-) create mode 100644 .gitignore 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 5c83a6dc..020f8bd2 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -25,14 +25,10 @@ type SchemaValidationFailure struct { // FieldPath is the JSONPath representation of the field location that failed validation (e.g., "$.user.email") FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"` - // KeywordLocation is the relative path to the JsonSchema keyword that failed validation - // This will be empty if the validation failure did not originate from JSON Schema validation + // 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"` - // AbsoluteKeywordLocation is the absolute path to the validation failure as exposed by the jsonschema library. - // This will be empty if the validation failure did not originate from JSON Schema validation - AbsoluteKeywordLocation string `json:"absoluteKeywordLocation,omitempty" yaml:"absoluteKeywordLocation,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 // the Context object held by the ValidationError object). diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index 2f5d7d31..2885369b 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -226,7 +226,6 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, - AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, OriginalJsonSchemaError: scErrs, } if schema != nil { diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 788f5d60..9282bd5e 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -77,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), - KeywordLocation: er.KeywordLocation, - AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, - OriginalJsonSchemaError: 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_schema.go b/schema_validation/validate_schema.go index 427f0520..b2def790 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -285,7 +285,6 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, - AbsoluteKeywordLocation: er.AbsoluteKeywordLocation, ReferenceSchema: string(renderedSchema), ReferenceObject: referenceObject, OriginalJsonSchemaError: jk, From a21d5ab7d0e1a737d1346c93a5fa6e834adc93d5 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 7 Nov 2025 15:21:21 -0800 Subject: [PATCH 8/9] Request body validation: remove SchemaValidationFailure from pre-validation errors, add KeywordLocation to schema violations Pre-validation errors (compilation, JSON decode, empty body) now correctly omit SchemaValidationFailure objects, as they don't represent actual schema constraint violations. Actual schema violations now include KeywordLocation (JSON Pointer path to the failing keyword) for better error context. Also fixed Location field to use er.InstanceLocation for consistency with schema_validation/validate_schema.go. --- requests/validate_body_test.go | 4 +- requests/validate_request.go | 70 +++++++++++++--------------------- 2 files changed, 28 insertions(+), 46 deletions(-) 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 06baf3b0..f20b8186 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 } @@ -235,16 +216,17 @@ 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, - OriginalJsonSchemaError: jk, - } + violation := &errors.SchemaValidationFailure{ + 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 { From 969efb8687256babe49e1c5f8cc0f384e28f0020 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 7 Nov 2025 15:46:09 -0800 Subject: [PATCH 9/9] Response body validation: remove SchemaValidationFailure from pre-validation errors, add KeywordLocation to schema violations Pre-validation errors (compilation, missing response, IO read, JSON decode) now correctly omit SchemaValidationFailure objects, as they don't represent actual schema constraint violations. Actual schema violations now include KeywordLocation (JSON Pointer path to the failing keyword) for better error context. Also fixed Location field to use er.InstanceLocation for consistency with request validation and schema validation. --- requests/validate_request.go | 22 +++++------ responses/validate_body_test.go | 3 +- responses/validate_response.go | 67 +++++++++++---------------------- 3 files changed, 34 insertions(+), 58 deletions(-) diff --git a/requests/validate_request.go b/requests/validate_request.go index f20b8186..cb89624b 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -216,17 +216,17 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) - violation := &errors.SchemaValidationFailure{ - 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, - } + violation := &errors.SchemaValidationFailure{ + 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 6a644da1..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 } @@ -252,10 +226,11 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors violation := &errors.SchemaValidationFailure{ Reason: errMsg, - Location: er.KeywordLocation, + 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,