From 6c03ca51c1591cee2a9282c69c5fd210b6cfd419 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Wed, 5 Nov 2025 17:11:13 -0800 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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, From 9bd05351e2055ed941b44448a17c4abd3eea862d Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 7 Nov 2025 16:25:33 -0800 Subject: [PATCH 07/15] Response headers: add SchemaValidationFailure with full OpenAPI path When a response header is marked as required in the OpenAPI schema and is missing from the response, this is a schema constraint violation. Added SchemaValidationFailure with full OpenAPI path context for KeywordLocation. Updated ValidateResponseHeaders signature to accept pathTemplate and statusCode to construct full JSON Pointer paths like: /paths/~1health/get/responses/200/headers/chicken-nuggets/required This makes header validation consistent with request/response body validation, which also uses full OpenAPI document paths for KeywordLocation. Note: Considered using relative paths (/header-name/required) but chose full paths for consistency with body validation patterns. Both approaches have tradeoffs documented in PR description. --- responses/validate_body.go | 2 +- responses/validate_headers.go | 16 ++++++++++++++++ responses/validate_headers_test.go | 6 +++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/responses/validate_body.go b/responses/validate_body.go index fc760dbb..87bb53a8 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -109,7 +109,7 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R if foundResponse != nil { // check for headers in the response if foundResponse.Headers != nil { - if ok, herrs := ValidateResponseHeaders(request, response, foundResponse.Headers); !ok { + if ok, herrs := ValidateResponseHeaders(request, response, foundResponse.Headers, pathFound, codeStr); !ok { validationErrors = append(validationErrors, herrs...) } } diff --git a/responses/validate_headers.go b/responses/validate_headers.go index ee284fa9..2d5499ca 100644 --- a/responses/validate_headers.go +++ b/responses/validate_headers.go @@ -23,6 +23,8 @@ func ValidateResponseHeaders( request *http.Request, response *http.Response, headers *orderedmap.Map[string, *v3.Header], + pathTemplate string, + statusCode string, opts ...config.Option, ) (bool, []*errors.ValidationError) { options := config.NewValidationOptions(opts...) @@ -53,6 +55,14 @@ func ValidateResponseHeaders( for name, header := range headers.FromOldest() { if header.Required { if _, ok := locatedHeaders[strings.ToLower(name)]; !ok { + // Construct full OpenAPI path for KeywordLocation + // e.g., /paths/~1health/get/responses/200/headers/chicken-nuggets/required + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + method := strings.ToLower(request.Method) + keywordLocation := fmt.Sprintf("/paths/%s/%s/responses/%s/headers/%s/required", + escapedPath, method, statusCode, name) + validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -63,6 +73,12 @@ func ValidateResponseHeaders( HowToFix: errors.HowToFixMissingHeader, RequestPath: request.URL.Path, RequestMethod: request.Method, + SchemaValidationErrors: []*errors.SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required header '%s' is missing", name), + FieldName: name, + InstancePath: []string{name}, + KeywordLocation: keywordLocation, + }}, }) } } diff --git a/responses/validate_headers_test.go b/responses/validate_headers_test.go index feb56001..ddcf5214 100644 --- a/responses/validate_headers_test.go +++ b/responses/validate_headers_test.go @@ -54,7 +54,7 @@ paths: headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors := ValidateResponseHeaders(request, response, headers) + valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) @@ -76,7 +76,7 @@ paths: headers = m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors = ValidateResponseHeaders(request, response, headers) + valid, errors = ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) @@ -125,7 +125,7 @@ paths: headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors := ValidateResponseHeaders(request, response, headers) + valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.True(t, valid) assert.Len(t, errors, 0) From f3c5bb1350075717a9b47c3a2a4161cd57467643 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Fri, 7 Nov 2025 18:29:44 -0800 Subject: [PATCH 08/15] Parameters: render path param schema once, pass to error functions for ReferenceSchema - Render parameter schema once in path_parameters.go instead of in each error function - Pass renderedSchema to all 8 path parameter error functions (bool, enum, integer, number, array variants) - Update Context field to use raw base.Schema (programmatic access) - Update ReferenceSchema field to use rendered JSON string (API consumers) - Use full OpenAPI JSON Pointer paths for KeywordLocation (e.g., /paths/~1users~1{id}/parameters/id/schema/type) - Serialize full schema objects for ReferenceSchema instead of just type strings - Update resolveNumber and resolveInteger helpers to accept and pass renderedSchema Note: This approach (Context=raw schema, ReferenceSchema=rendered string) will be reviewed later for consistency across the codebase --- errors/parameter_errors.go | 117 +++++++++++++++++++++++++++++--- errors/parameter_errors_test.go | 16 ++--- parameters/path_parameters.go | 56 +++++++++------ 3 files changed, 154 insertions(+), 35 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 2f9768a4..4bf67547 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -507,7 +507,12 @@ func IncorrectHeaderParamArrayNumber( } } -func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -518,15 +523,28 @@ func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/enum", escapedPath, param.Name) + var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -537,10 +555,22 @@ func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *V SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -552,10 +582,22 @@ func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schem ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -566,12 +608,24 @@ func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -582,12 +636,24 @@ func IncorrectPathParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayInteger( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -598,12 +664,24 @@ func IncorrectPathParamArrayInteger( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + // Build full OpenAPI path for KeywordLocation + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -614,10 +692,26 @@ func IncorrectPathParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func PathParameterMissing(param *v3.Parameter) *ValidationError { +func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath string) *ValidationError { + // Build instance path showing the URL structure + actualSegments := strings.Split(strings.Trim(actualPath, "/"), "/") + + // Build keyword location with path template (JSON Pointer encoding: / becomes ~1) + encodedPath := strings.ReplaceAll(pathTemplate, "~", "~0") // Escape ~ first + encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") // Then escape / + encodedPath = strings.TrimPrefix(encodedPath, "~1") // Remove leading ~1 + keywordLoc := fmt.Sprintf("/paths/%s/parameters/%s/required", encodedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -627,5 +721,12 @@ func PathParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required path parameter '%s' is missing from path '%s'", param.Name, actualPath), + FieldName: param.Name, + FieldPath: "", + InstancePath: actualSegments, + KeywordLocation: keywordLoc, + }}, } } diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index 4f8304a6..f76106c6 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -914,7 +914,7 @@ func TestIncorrectPathParamBool(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamBool(param, "milky", highSchema) + err := IncorrectPathParamBool(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -945,7 +945,7 @@ items: } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} - err := IncorrectPathParamEnum(param, "milky", highSchema) + err := IncorrectPathParamEnum(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -970,7 +970,7 @@ func TestIncorrectPathParamNumber(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamNumber(param, "milky", highSchema) + err := IncorrectPathParamNumber(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -995,7 +995,7 @@ func TestIncorrectPathParamInteger(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamInteger(param, "milky", highSchema) + err := IncorrectPathParamInteger(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1021,7 +1021,7 @@ func TestIncorrectPathParamArrayNumber(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayNumber(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1047,7 +1047,7 @@ func TestIncorrectPathParamArrayInteger(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayInteger(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayInteger(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1073,7 +1073,7 @@ func TestIncorrectPathParamArrayBoolean(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayBoolean(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1098,7 +1098,7 @@ func TestPathParameterMissing(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := PathParameterMissing(param) + err := PathParameterMissing(param, "/test/{testQueryParam}", "/test/") // Validate the error require.NotNil(t, err) diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 31f7e3ce..78d83393 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "net/url" @@ -129,7 +130,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p if decodedParamValue == "" { // Mandatory path parameter cannot be empty if p.Required != nil && *p.Required { - validationErrors = append(validationErrors, errors.PathParameterMissing(p)) + validationErrors = append(validationErrors, errors.PathParameterMissing(p, pathValue, request.URL.Path)) break } continue @@ -138,6 +139,14 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // extract the schema from the parameter sch := p.Schema.Schema() + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + // check enum (if present) enumCheck := func(decodedValue string) { matchFound := false @@ -149,7 +158,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectPathParamEnum(p, strings.ToLower(decodedValue), sch)) + errors.IncorrectPathParamEnum(p, strings.ToLower(decodedValue), sch, pathValue, renderedSchema)) } } @@ -180,7 +189,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p case helpers.Integer: // simple use case is already handled in find param. - rawParamValue, paramValueParsed, err := v.resolveInteger(sch, p, isLabel, isMatrix, decodedParamValue) + rawParamValue, paramValueParsed, err := v.resolveInteger(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break @@ -203,7 +212,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p case helpers.Number: // simple use case is already handled in find param. - rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, decodedParamValue) + rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break @@ -228,13 +237,13 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p if isLabel && p.Style == helpers.LabelStyle { if _, err := strconv.ParseBool(decodedParamValue[1:]); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedParamValue[1:], sch)) + errors.IncorrectPathParamBool(p, decodedParamValue[1:], sch, pathValue, renderedSchema)) } } if isSimple { if _, err := strconv.ParseBool(decodedParamValue); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedParamValue, sch)) + errors.IncorrectPathParamBool(p, decodedParamValue, sch, pathValue, renderedSchema)) } } if isMatrix && p.Style == helpers.MatrixStyle { @@ -242,7 +251,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) if _, err := strconv.ParseBool(decodedForMatrix); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedForMatrix, sch)) + errors.IncorrectPathParamBool(p, decodedForMatrix, sch, pathValue, renderedSchema)) } } case helpers.Object: @@ -290,6 +299,15 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // extract the items schema in order to validate the array items. if sch.Items != nil && sch.Items.IsA() { iSch := sch.Items.A.Schema() + + // Render items schema once for ReferenceSchema field in array errors + var renderedItemsSchema string + if iSch != nil { + rendered, _ := iSch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + for n := range iSch.Type { // determine how to explode the array var arrayValues []string @@ -317,14 +335,14 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p for pv := range arrayValues { if _, err := strconv.ParseInt(arrayValues[pv], 10, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayInteger(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayInteger(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Number: for pv := range arrayValues { if _, err := strconv.ParseFloat(arrayValues[pv], 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayNumber(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayNumber(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Boolean: @@ -332,7 +350,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p bc := len(validationErrors) if _, err := strconv.ParseBool(arrayValues[pv]); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } if len(validationErrors) == bc { @@ -340,7 +358,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // need to catch this edge case. if arrayValues[pv] == "0" || arrayValues[pv] == "1" { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } } @@ -364,11 +382,11 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p return true, nil } -func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string) (string, float64, []*errors.ValidationError) { +func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, float64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseFloat(paramValue[1:], 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } @@ -377,22 +395,22 @@ func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabe paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue, sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } -func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string) (string, int64, []*errors.ValidationError) { +func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, int64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseInt(paramValue[1:], 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } @@ -401,13 +419,13 @@ func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLab paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue, sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } From 2024c983bc9a963673a321f52a5dfef0f332cdec Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Thu, 13 Nov 2025 15:31:04 -0800 Subject: [PATCH 09/15] Remove working-memory/ from .gitignore This directory was temporarily added during development but should not be ignored in version control. --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 00a9078d..44d8ffcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -# Working memory and debug artifacts -working-memory/ \ No newline at end of file +# Working memory and debug artifacts \ No newline at end of file From 27672244f6a93289ff22c44d9a7f6dc613d3fb34 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 08:42:59 -0800 Subject: [PATCH 10/15] Makes all query params return a SchemaValidationError Changes: - Remove 'Build full OpenAPI path for KeywordLocation' comments - Remove inline comments from previous commits - Add renderedSchema parameter to QueryParameterMissing - Set ReferenceSchema field in QueryParameterMissing SchemaValidationFailure - Render schema for missing required parameters before creating error - Update tests to pass renderedSchema parameter --- errors/parameter_errors.go | 214 +++++++++++++++++++++++++---- errors/parameter_errors_test.go | 28 ++-- parameters/query_parameters.go | 56 ++++++-- parameters/validate_parameter.go | 39 +++++- parameters/validation_functions.go | 24 ++-- 5 files changed, 295 insertions(+), 66 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 4bf67547..44c6c7e5 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -76,7 +76,12 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE } } -func QueryParameterMissing(param *v3.Parameter) *ValidationError { +func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/required", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -86,6 +91,14 @@ func QueryParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required query parameter '%s' is missing", param.Name), + FieldName: param.Name, + FieldPath: "", + InstancePath: []string{}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } @@ -135,8 +148,13 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema) } func IncorrectQueryParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -147,10 +165,22 @@ func IncorrectQueryParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64) *ValidationError { +func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/maxItems", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -161,10 +191,22 @@ func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expec SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMaxItems, expected), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array has %d items, but maximum is %d", actual, expected), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64) *ValidationError { +func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/minItems", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -175,10 +217,22 @@ func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expec SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMinItems, expected), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array has %d items, but minimum is %d", actual, expected), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string) *ValidationError { +func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/uniqueItems", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -188,6 +242,13 @@ func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, dupli SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: "Ensure the array values are all unique", + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array contains duplicate values: %s", duplicates), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } @@ -208,8 +269,13 @@ func IncorrectCookieParamArrayBoolean( } func IncorrectQueryParamArrayInteger( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -220,12 +286,24 @@ func IncorrectQueryParamArrayInteger( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectQueryParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -236,6 +314,13 @@ func IncorrectQueryParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } @@ -255,7 +340,12 @@ func IncorrectCookieParamArrayNumber( } } -func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/content/application~1json/schema", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -266,10 +356,22 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema SpecCol: param.GoLow().FindContent(helpers.JSONContentType).ValueNode.Column, Context: sch, HowToFix: HowToFixInvalidJSON, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not valid JSON", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -281,10 +383,22 @@ func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -296,10 +410,22 @@ func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -311,15 +437,28 @@ func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -331,10 +470,17 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedItemsSchema string) *ValidationError { var enums []string // look at that model fly! for i := range param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value { @@ -342,6 +488,12 @@ func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Sche fmt.Sprint(param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value[i].Value.Value)) } validEnums := strings.Join(enums, ", ") + + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/enum", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -352,10 +504,22 @@ func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Sche SpecCol: param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.KeyNode.Line, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/allowReserved", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -366,6 +530,13 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema) * SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixReservedValues, url.QueryEscape(ef)), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' contains reserved characters but allowReserved is false", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } @@ -508,7 +679,6 @@ func IncorrectHeaderParamArrayNumber( } func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) @@ -534,7 +704,6 @@ func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, } func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/enum", escapedPath, param.Name) @@ -566,7 +735,6 @@ func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pa } func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) @@ -593,7 +761,6 @@ func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schem } func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) @@ -621,7 +788,6 @@ func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema func IncorrectPathParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) @@ -649,7 +815,6 @@ func IncorrectPathParamArrayNumber( func IncorrectPathParamArrayInteger( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) @@ -677,7 +842,6 @@ func IncorrectPathParamArrayInteger( func IncorrectPathParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { - // Build full OpenAPI path for KeywordLocation escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) @@ -703,13 +867,11 @@ func IncorrectPathParamArrayBoolean( } func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath string) *ValidationError { - // Build instance path showing the URL structure actualSegments := strings.Split(strings.Trim(actualPath, "/"), "/") - // Build keyword location with path template (JSON Pointer encoding: / becomes ~1) - encodedPath := strings.ReplaceAll(pathTemplate, "~", "~0") // Escape ~ first - encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") // Then escape / - encodedPath = strings.TrimPrefix(encodedPath, "~1") // Remove leading ~1 + encodedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") + encodedPath = strings.TrimPrefix(encodedPath, "~1") keywordLoc := fmt.Sprintf("/paths/%s/parameters/%s/required", encodedPath, param.Name) return &ValidationError{ diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index f76106c6..afc953a5 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -119,7 +119,7 @@ func TestQueryParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function - err := QueryParameterMissing(param) + err := QueryParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -223,7 +223,7 @@ func TestIncorrectQueryParamArrayBoolean(t *testing.T) { schema := base.NewSchema(s) // Call the function with an invalid boolean value in the array - err := IncorrectQueryParamArrayBoolean(param, "notBoolean", schema, schema.Items.A.Schema()) + err := IncorrectQueryParamArrayBoolean(param, "notBoolean", schema, schema.Items.A.Schema(), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -375,7 +375,7 @@ func TestIncorrectQueryParamArrayInteger(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array - err := IncorrectQueryParamArrayInteger(param, "notNumber", s, itemsSchema) + err := IncorrectQueryParamArrayInteger(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -394,7 +394,7 @@ func TestIncorrectQueryParamArrayNumber(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array - err := IncorrectQueryParamArrayNumber(param, "notNumber", s, itemsSchema) + err := IncorrectQueryParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -531,7 +531,7 @@ func TestIncorrectParamEncodingJSON(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid JSON value - err := IncorrectParamEncodingJSON(param, "invalidJSON", base.NewSchema(baseSchema)) + err := IncorrectParamEncodingJSON(param, "invalidJSON", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -560,7 +560,7 @@ func TestIncorrectQueryParamBool(t *testing.T) { }) // Call the function with an invalid boolean value - err := IncorrectQueryParamBool(param, "notBoolean", base.NewSchema(baseSchema)) + err := IncorrectQueryParamBool(param, "notBoolean", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -576,7 +576,7 @@ func TestInvalidQueryParamNumber(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value - err := InvalidQueryParamNumber(param, "notNumber", base.NewSchema(baseSchema)) + err := InvalidQueryParamNumber(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -592,7 +592,7 @@ func TestInvalidQueryParamInteger(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value - err := InvalidQueryParamInteger(param, "notNumber", base.NewSchema(baseSchema)) + err := InvalidQueryParamInteger(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -617,7 +617,7 @@ func TestIncorrectQueryParamEnum(t *testing.T) { param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} // Call the function with an invalid enum value - err := IncorrectQueryParamEnum(param, "invalidEnum", highSchema) + err := IncorrectQueryParamEnum(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -646,7 +646,7 @@ func TestIncorrectQueryParamEnumArray(t *testing.T) { } // Call the function with an invalid enum value - err := IncorrectQueryParamEnumArray(param, "invalidEnum", highSchema) + err := IncorrectQueryParamEnumArray(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -669,7 +669,7 @@ func TestIncorrectReservedValues(t *testing.T) { param := createMockParameter() param.Name = "borked::?^&*" - err := IncorrectReservedValues(param, "borked::?^&*", highSchema) + err := IncorrectReservedValues(param, "borked::?^&*", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -1124,7 +1124,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayMaxNumItems(param, param.Schema.Schema(), 10, 25) + err := IncorrectParamArrayMaxNumItems(param, param.Schema.Schema(), 10, 25, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -1150,7 +1150,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayMinNumItems(param, param.Schema.Schema(), 10, 5) + err := IncorrectParamArrayMinNumItems(param, param.Schema.Schema(), 10, 5, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -1176,7 +1176,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayUniqueItems(param, param.Schema.Schema(), "fish, cake") + err := IncorrectParamArrayUniqueItems(param, param.Schema.Schema(), "fish, cake", "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 888cbc8c..91fe90c3 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -70,6 +70,9 @@ func (v *paramValidator) ValidateQueryParamsWithPathItem(request *http.Request, } } + // Get operation from request method (lowercase for JSON Pointer) + operation := strings.ToLower(request.Method) + // look through the params for the query key doneLooking: for p := range params { @@ -101,6 +104,15 @@ doneLooking: break } } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type // for each param, check each type @@ -113,34 +125,34 @@ doneLooking: if !params[p].AllowReserved { if rxRxp.MatchString(ef) && params[p].IsExploded() { validationErrors = append(validationErrors, - errors.IncorrectReservedValues(params[p], ef, sch)) + errors.IncorrectReservedValues(params[p], ef, sch, pathValue, operation, renderedSchema)) } } for _, ty := range pType { switch ty { case helpers.String: - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, ef, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, ef, params[p], pathValue, operation, renderedSchema)...) case helpers.Integer: efF, err := strconv.ParseInt(ef, 10, 64) if err != nil { validationErrors = append(validationErrors, - errors.InvalidQueryParamInteger(params[p], ef, sch)) + errors.InvalidQueryParamInteger(params[p], ef, sch, pathValue, operation, renderedSchema)) break } - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Number: efF, err := strconv.ParseFloat(ef, 64) if err != nil { validationErrors = append(validationErrors, - errors.InvalidQueryParamNumber(params[p], ef, sch)) + errors.InvalidQueryParamNumber(params[p], ef, sch, pathValue, operation, renderedSchema)) break } - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Boolean: if _, err := strconv.ParseBool(ef); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamBool(params[p], ef, sch)) + errors.IncorrectQueryParamBool(params[p], ef, sch, pathValue, operation, renderedSchema)) } case helpers.Object: @@ -165,7 +177,7 @@ doneLooking: encodedObj = make(map[string]interface{}) if err := json.Unmarshal([]byte(ef), &encodedParams); err != nil { validationErrors = append(validationErrors, - errors.IncorrectParamEncodingJSON(params[p], ef, sch)) + errors.IncorrectParamEncodingJSON(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } encodedObj[params[p].Name] = encodedParams @@ -195,7 +207,7 @@ doneLooking: // only check if items is a schema, not a boolean if sch.Items != nil && sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options)...) + ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options, pathValue, operation, renderedSchema)...) } } } @@ -225,7 +237,23 @@ doneLooking: } // if there is no match, check if the param is required or not. if params[p].Required != nil && *params[p].Required { - validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p])) + // Render schema for missing parameter + var sch *base.Schema + if params[p].Schema != nil { + sch = params[p].Schema.Schema() + } else { + for pair := orderedmap.First(params[p].Content); pair != nil; pair = pair.Next() { + sch = pair.Value().Schema.Schema() + break + } + } + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p], pathValue, operation, renderedSchema)) } } } @@ -239,7 +267,7 @@ doneLooking: return true, nil } -func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter) (validationErrors []*errors.ValidationError) { +func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter, pathTemplate string, operation string, renderedSchema string) (validationErrors []*errors.ValidationError) { // check if the param is within an enum if sch.Enum != nil { matchFound := false @@ -250,11 +278,11 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, } } if !matchFound { - return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch)} + return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch, pathTemplate, operation, renderedSchema)} } } - return ValidateSingleParameterSchema( + return ValidateSingleParameterSchemaWithPath( sch, parsedParam, "Query parameter", @@ -263,5 +291,7 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options, + pathTemplate, + operation, ) } diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index 2885369b..df5ae8b4 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -33,6 +33,21 @@ func ValidateSingleParameterSchema( validationType string, subValType string, o *config.ValidationOptions, +) (validationErrors []*errors.ValidationError) { + return ValidateSingleParameterSchemaWithPath(schema, rawObject, entity, reasonEntity, name, validationType, subValType, o, "", "") +} + +func ValidateSingleParameterSchemaWithPath( + schema *base.Schema, + rawObject any, + entity string, + reasonEntity string, + name string, + validationType string, + subValType string, + o *config.ValidationOptions, + pathTemplate string, + operation string, ) (validationErrors []*errors.ValidationError) { // Get the JSON Schema for the parameter definition. jsonSchema, err := buildJsonRender(schema) @@ -50,7 +65,7 @@ func ValidateSingleParameterSchema( scErrs := jsch.Validate(rawObject) var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { - validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType) + validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, pathTemplate, operation) } return validationErrors } @@ -183,7 +198,7 @@ func ValidateParameterSchema( } var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { - validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType) + validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, "", "") } // if there are no validationErrors, check that the supplied value is even JSON @@ -207,7 +222,7 @@ func ValidateParameterSchema( return validationErrors } -func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string) (validationErrors []*errors.ValidationError) { +func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string, pathTemplate string, operation string) (validationErrors []*errors.ValidationError) { // flatten the validationErrors schFlatErrs := scErrs.BasicOutput().Errors var schemaValidationErrors []*errors.SchemaValidationFailure @@ -219,19 +234,33 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val continue // ignore this error, it's not useful } + // Construct full OpenAPI path for KeywordLocation if pathTemplate and operation are provided + keywordLocation := er.KeywordLocation + if pathTemplate != "" && operation != "" && validationType == helpers.ParameterValidation { + // Build full OpenAPI path: /paths/{escapedPath}/{operation}/parameters/{paramName}/schema{relativeKeywordLocation} + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading ~1 + + // er.KeywordLocation is relative to the schema (e.g., "/minLength" or "/enum") + // Prepend the full OpenAPI path + keywordLocation = fmt.Sprintf("/paths/%s/%s/parameters/%s/schema%s", escapedPath, strings.ToLower(operation), name, er.KeywordLocation) + } + fail := &errors.SchemaValidationFailure{ Reason: errMsg, Location: er.KeywordLocation, // DEPRECATED FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - KeywordLocation: er.KeywordLocation, + KeywordLocation: keywordLocation, OriginalJsonSchemaError: scErrs, } if schema != nil { rendered, err := schema.RenderInline() if err == nil && rendered != nil { - fail.ReferenceSchema = string(rendered) + renderedBytes, _ := json.Marshal(rendered) + fail.ReferenceSchema = string(renderedBytes) } } schemaValidationErrors = append(schemaValidationErrors, fail) diff --git a/parameters/validation_functions.go b/parameters/validation_functions.go index f1080a59..84eeced1 100644 --- a/parameters/validation_functions.go +++ b/parameters/validation_functions.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "slices" "strconv" @@ -99,11 +100,18 @@ func ValidateHeaderArray( // ValidateQueryArray will validate a query parameter that is an array func ValidateQueryArray( - sch *base.Schema, param *v3.Parameter, ef string, contentWrapped bool, validationOptions *config.ValidationOptions, + sch *base.Schema, param *v3.Parameter, ef string, contentWrapped bool, validationOptions *config.ValidationOptions, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // check for an exploded bit on the schema. // if it's exploded, then we need to check each item in the array // if it's not exploded, then we need to check the whole array as a string @@ -141,7 +149,7 @@ func ValidateQueryArray( } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectQueryParamEnumArray(param, item, sch)) + errors.IncorrectQueryParamEnumArray(param, item, sch, pathTemplate, operation, renderedItemsSchema)) } } } @@ -165,7 +173,7 @@ func ValidateQueryArray( case helpers.Integer: if _, err := strconv.ParseInt(item, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayInteger(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayInteger(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? @@ -173,7 +181,7 @@ func ValidateQueryArray( case helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? @@ -182,7 +190,7 @@ func ValidateQueryArray( case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Object: validationErrors = append(validationErrors, @@ -207,14 +215,14 @@ func ValidateQueryArray( if sch.MaxItems != nil { if len(items) > int(*sch.MaxItems) { validationErrors = append(validationErrors, - errors.IncorrectParamArrayMaxNumItems(param, sch, *sch.MaxItems, int64(len(items)))) + errors.IncorrectParamArrayMaxNumItems(param, sch, *sch.MaxItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } if sch.MinItems != nil { if len(items) < int(*sch.MinItems) { validationErrors = append(validationErrors, - errors.IncorrectParamArrayMinNumItems(param, sch, *sch.MinItems, int64(len(items)))) + errors.IncorrectParamArrayMinNumItems(param, sch, *sch.MinItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } @@ -222,7 +230,7 @@ func ValidateQueryArray( if sch.UniqueItems != nil { if *sch.UniqueItems && !uniqueItems { validationErrors = append(validationErrors, - errors.IncorrectParamArrayUniqueItems(param, sch, strings.Join(duplicates, ", "))) + errors.IncorrectParamArrayUniqueItems(param, sch, strings.Join(duplicates, ", "), pathTemplate, operation, renderedSchema)) } } return validationErrors From a1576bd0bfdffa3e9768d61605013a2581044cdc Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 09:15:07 -0800 Subject: [PATCH 11/15] feat: add SchemaValidationFailure context to header parameter errors Adds full OpenAPI context to all 7 header parameter error functions: - HeaderParameterMissing - HeaderParameterCannotBeDecoded (now includes SchemaValidationFailure) - IncorrectHeaderParamEnum - InvalidHeaderParamInteger - InvalidHeaderParamNumber - IncorrectHeaderParamBool - IncorrectHeaderParamArrayBoolean - IncorrectHeaderParamArrayNumber All errors now include: - KeywordLocation: Full JSON Pointer from OpenAPI root (e.g., /paths/{path}/{method}/parameters/{name}/schema/type) - ReferenceSchema: Rendered schema as JSON string - Context: Raw base.Schema object - Proper FieldName and InstancePath Updated ValidateHeaderArray to accept and pass path/operation/schema context. Updated all test cases to pass new required parameters. --- errors/parameter_errors.go | 199 +++++++++++++++++++++++++++-- errors/parameter_errors_test.go | 28 ++-- parameters/header_parameters.go | 39 ++++-- parameters/validation_functions.go | 32 +++-- 4 files changed, 252 insertions(+), 46 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 44c6c7e5..9bbcee85 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -102,7 +102,12 @@ func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation s } } -func HeaderParameterMissing(param *v3.Parameter) *ValidationError { +func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/required", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -112,10 +117,23 @@ func HeaderParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required header parameter '%s' is missing", param.Name), + FieldName: param.Name, + FieldPath: "", + InstancePath: []string{}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string) *ValidationError { +func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -125,15 +143,28 @@ func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string) *Validation SpecLine: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, SpecCol: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, HowToFix: HowToFixInvalidEncoding, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Header value '%s' cannot be decoded as object (malformed encoding)", val), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -144,6 +175,13 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } @@ -253,8 +291,13 @@ func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, dupli } func IncorrectCookieParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -265,6 +308,13 @@ func IncorrectCookieParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } @@ -325,8 +375,13 @@ func IncorrectQueryParamArrayNumber( } func IncorrectCookieParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -337,6 +392,13 @@ func IncorrectCookieParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } @@ -540,7 +602,12 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, p } } -func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -552,10 +619,22 @@ func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -567,10 +646,22 @@ func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -581,10 +672,22 @@ func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -595,10 +698,22 @@ func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -610,10 +725,22 @@ func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -624,15 +751,28 @@ func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -643,12 +783,24 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectHeaderParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -659,12 +811,24 @@ func IncorrectHeaderParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectHeaderParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -675,6 +839,13 @@ func IncorrectHeaderParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index afc953a5..4fb5b9b6 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -134,7 +134,7 @@ func TestHeaderParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function - err := HeaderParameterMissing(param) + err := HeaderParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -150,7 +150,7 @@ func TestHeaderParameterCannotBeDecoded(t *testing.T) { val := "malformed_header_value" // Call the function - err := HeaderParameterCannotBeDecoded(param, val) + err := HeaderParameterCannotBeDecoded(param, val, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -187,7 +187,7 @@ func TestIncorrectHeaderParamEnum(t *testing.T) { schema := base.NewSchema(s) // Call the function with an invalid enum value - err := IncorrectHeaderParamEnum(param, "invalidEnum", schema) + err := IncorrectHeaderParamEnum(param, "invalidEnum", schema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -314,7 +314,7 @@ func TestIncorrectCookieParamArrayBoolean(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid boolean value in the array - err := IncorrectCookieParamArrayBoolean(param, "notBoolean", s, itemsSchema) + err := IncorrectCookieParamArrayBoolean(param, "notBoolean", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -455,7 +455,7 @@ func TestIncorrectCookieParamArrayNumber(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the cookie array - err := IncorrectCookieParamArrayNumber(param, "notNumber", s, itemsSchema) + err := IncorrectCookieParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -692,7 +692,7 @@ func TestInvalidHeaderParamInteger(t *testing.T) { param := createMockParameter() param.Name = "bunny" - err := InvalidHeaderParamInteger(param, "bunmy", highSchema) + err := InvalidHeaderParamInteger(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -715,7 +715,7 @@ func TestInvalidHeaderParamNumber(t *testing.T) { param := createMockParameter() param.Name = "bunny" - err := InvalidHeaderParamNumber(param, "bunmy", highSchema) + err := InvalidHeaderParamNumber(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -738,7 +738,7 @@ func TestInvalidCookieParamNumber(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := InvalidCookieParamNumber(param, "milky", highSchema) + err := InvalidCookieParamNumber(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -761,7 +761,7 @@ func TestInvalidCookieParamInteger(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := InvalidCookieParamInteger(param, "milky", highSchema) + err := InvalidCookieParamInteger(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -784,7 +784,7 @@ func TestIncorrectHeaderParamBool(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := IncorrectHeaderParamBool(param, "milky", highSchema) + err := IncorrectHeaderParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -807,7 +807,7 @@ func TestIncorrectCookieParamBool(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := IncorrectCookieParamBool(param, "milky", highSchema) + err := IncorrectCookieParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -837,7 +837,7 @@ items: } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} - err := IncorrectCookieParamEnum(param, "milky", highSchema) + err := IncorrectCookieParamEnum(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -863,7 +863,7 @@ func TestIncorrectHeaderParamArrayBoolean(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectHeaderParamArrayBoolean(param, "milky", highSchema, nil) + err := IncorrectHeaderParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -889,7 +889,7 @@ func TestIncorrectHeaderParamArrayNumber(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectHeaderParamArrayNumber(param, "milky", highSchema, nil) + err := IncorrectHeaderParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index a4c56a1d..e370bb2a 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -47,6 +48,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, var validationErrors []*errors.ValidationError seenHeaders := make(map[string]bool) + operation := strings.ToLower(request.Method) for _, p := range params { if p.In == helpers.Header { @@ -57,6 +59,15 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if p.Schema != nil { sch = p.Schema.Schema() } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type for _, ty := range pType { @@ -64,7 +75,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, case helpers.Integer: if _, err := strconv.ParseInt(param, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidHeaderParamInteger(p, strings.ToLower(param), sch)) + errors.InvalidHeaderParamInteger(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum @@ -78,14 +89,14 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(param, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidHeaderParamNumber(p, strings.ToLower(param), sch)) + errors.InvalidHeaderParamNumber(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum @@ -99,14 +110,14 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(param); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamBool(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamBool(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } case helpers.Object: @@ -124,7 +135,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if len(encodedObj) == 0 { validationErrors = append(validationErrors, - errors.HeaderParameterCannotBeDecoded(p, strings.ToLower(param))) + errors.HeaderParameterCannotBeDecoded(p, strings.ToLower(param), pathValue, operation, renderedSchema)) break } @@ -145,7 +156,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if !p.IsExploded() { // only unexploded arrays are supported for cookie params if sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateHeaderArray(sch, p, param)...) + ValidateHeaderArray(sch, p, param, pathValue, operation, renderedSchema)...) } } @@ -163,7 +174,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } } @@ -177,7 +188,17 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } } else { if p.Required != nil && *p.Required { - validationErrors = append(validationErrors, errors.HeaderParameterMissing(p)) + // Render schema for missing required parameter + var renderedSchema string + if p.Schema != nil { + sch := p.Schema.Schema() + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + } + validationErrors = append(validationErrors, errors.HeaderParameterMissing(p, pathValue, operation, renderedSchema)) } } } diff --git a/parameters/validation_functions.go b/parameters/validation_functions.go index 84eeced1..f931588f 100644 --- a/parameters/validation_functions.go +++ b/parameters/validation_functions.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/pb33f/libopenapi/datamodel/high/v3" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" @@ -20,11 +20,18 @@ import ( // ValidateCookieArray will validate a cookie parameter that is an array func ValidateCookieArray( - sch *base.Schema, param *v3.Parameter, value string, + sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) @@ -36,18 +43,18 @@ func ValidateCookieArray( case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. @@ -60,11 +67,18 @@ func ValidateCookieArray( // ValidateHeaderArray will validate a header parameter that is an array func ValidateHeaderArray( - sch *base.Schema, param *v3.Parameter, value string, + sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) @@ -76,18 +90,18 @@ func ValidateHeaderArray( case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. From d963a2dcdd9f9ae4d20b8fd91b66809c02394f74 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 09:15:15 -0800 Subject: [PATCH 12/15] feat: add SchemaValidationFailure context to cookie parameter errors Adds full OpenAPI context to all 6 cookie parameter error functions: - InvalidCookieParamInteger - InvalidCookieParamNumber - IncorrectCookieParamBool - IncorrectCookieParamEnum - IncorrectCookieParamArrayBoolean - IncorrectCookieParamArrayNumber All errors now include: - KeywordLocation: Full JSON Pointer from OpenAPI root (e.g., /paths/{path}/{method}/parameters/{name}/schema/type) - ReferenceSchema: Rendered schema as JSON string - Context: Raw base.Schema object - Proper FieldName and InstancePath Updated ValidateCookieArray to accept and pass path/operation/schema context. Updated all test cases to pass new required parameters. This completes consistent SchemaValidationFailure population across all parameter types (path, query, header, cookie). --- parameters/cookie_parameters.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/parameters/cookie_parameters.go b/parameters/cookie_parameters.go index 8f74b9ff..cd85f443 100644 --- a/parameters/cookie_parameters.go +++ b/parameters/cookie_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -43,6 +44,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError + operation := strings.ToLower(request.Method) for _, p := range params { if p.In == helpers.Cookie { for _, cookie := range request.Cookies() { @@ -52,6 +54,15 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, if p.Schema != nil { sch = p.Schema.Schema() } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type for _, ty := range pType { @@ -59,7 +70,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, case helpers.Integer: if _, err := strconv.ParseInt(cookie.Value, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidCookieParamInteger(p, strings.ToLower(cookie.Value), sch)) + errors.InvalidCookieParamInteger(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // check if enum is in range @@ -73,13 +84,13 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(cookie.Value, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidCookieParamNumber(p, strings.ToLower(cookie.Value), sch)) + errors.InvalidCookieParamNumber(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // check if enum is in range @@ -93,13 +104,13 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(cookie.Value); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamBool(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamBool(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } case helpers.Object: if !p.IsExploded() { @@ -125,7 +136,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, // only check if items is a schema, not a boolean if sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateCookieArray(sch, p, cookie.Value)...) + ValidateCookieArray(sch, p, cookie.Value, pathValue, operation, renderedSchema)...) } } @@ -143,7 +154,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } } From 6fde5372cb4dcc9f13d8ac932bb85fd478db6c58 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 09:19:51 -0800 Subject: [PATCH 13/15] refactor: remove deprecated Location field from SchemaValidationFailure Removed the deprecated Location field entirely from SchemaValidationFailure struct and updated all code and tests to use KeywordLocation instead. Changes: - Removed Location field from SchemaValidationFailure struct - Updated Error() method to use FieldPath instead of Location - Removed .Location assignment in schema_validation/validate_document.go - Updated all test assertions to use KeywordLocation instead of Location - Updated tests to reflect that schema compilation errors do not have SchemaValidationFailure objects (they were removed in earlier commits) This completes the transition to the new error reporting model where: - KeywordLocation: Full JSON Pointer to schema keyword (for all schema violations) - FieldPath: JSONPath to the failing instance (for body validation) - InstancePath: Structured path segments to failing instance - Location field: Removed entirely --- errors/validation_error.go | 9 +++-- parameters/query_parameters_test.go | 2 +- parameters/validate_parameter_test.go | 37 ++++++++++----------- requests/validate_request.go | 9 +++-- responses/validate_body_test.go | 17 ++++------ responses/validate_response.go | 31 +++++++++-------- schema_validation/validate_document.go | 5 ++- schema_validation/validate_document_test.go | 8 ++--- validator_test.go | 2 +- 9 files changed, 53 insertions(+), 67 deletions(-) diff --git a/errors/validation_error.go b/errors/validation_error.go index 020f8bd2..685232bc 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -50,15 +50,14 @@ type SchemaValidationFailure struct { // 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 func (s *SchemaValidationFailure) Error() string { - return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location) + if s.FieldPath != "" { + return fmt.Sprintf("Reason: %s, FieldPath: %s", s.Reason, s.FieldPath) + } + return fmt.Sprintf("Reason: %s", s.Reason) } // ValidationError is a struct that contains all the information about a validation error. diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 137969c6..8165995b 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -1729,7 +1729,7 @@ paths: assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as an object, "+ "however it failed to pass a schema validation", errors[0].Reason) assert.Equal(t, "missing properties 'vinegar', 'chips'", errors[0].SchemaValidationErrors[0].Reason) - assert.Equal(t, "/required", errors[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/required", errors[0].SchemaValidationErrors[0].KeywordLocation) } func TestNewValidator_QueryParamValidTypeObjectPropType_Invalid(t *testing.T) { diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index e076b881..3ed2a838 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -2,6 +2,7 @@ package parameters import ( "net/http" + "strings" "sync" "testing" @@ -264,7 +265,7 @@ func TestUnifiedErrorFormatWithFormatValidation(t *testing.T) { // verify unified error format - SchemaValidationErrors should be populated assert.Len(t, valErrs[0].SchemaValidationErrors, 1) assert.Contains(t, valErrs[0].SchemaValidationErrors[0].Reason, "is not valid email") - assert.Equal(t, "/format", valErrs[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/format", valErrs[0].SchemaValidationErrors[0].KeywordLocation) assert.NotEmpty(t, valErrs[0].SchemaValidationErrors[0].ReferenceSchema) } @@ -467,16 +468,13 @@ func TestComplexRegexSchemaCompilationError(t *testing.T) { found := false for _, err := range valErrs { if err.ParameterName == "complexParam" && - err.SchemaValidationErrors != nil && - len(err.SchemaValidationErrors) > 0 { - for _, schemaErr := range err.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" && - schemaErr.Reason != "" { - found = true - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, err.HowToFix, "complex regex patterns") - break - } + (err.SchemaValidationErrors == nil || len(err.SchemaValidationErrors) == 0) { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(err.Reason, "failed to compile JSON schema") { + found = true + assert.Contains(t, err.Reason, "failed to compile JSON schema") + assert.Contains(t, err.HowToFix, "complex regex patterns") + break } } } @@ -555,15 +553,14 @@ func TestValidateParameterSchema_SchemaCompilationFailure(t *testing.T) { for _, validationError := range validationErrors { if validationError.ParameterName == "failParam" && validationError.ValidationSubType == helpers.ParameterValidationQuery && - validationError.SchemaValidationErrors != nil { - for _, schemaErr := range validationError.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" { - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, validationError.HowToFix, "complex regex patterns") - assert.Equal(t, "Query parameter 'failParam' failed schema compilation", validationError.Message) - found = true - break - } + (validationError.SchemaValidationErrors == nil || len(validationError.SchemaValidationErrors) == 0) { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(validationError.Reason, "failed to compile JSON schema") { + assert.Contains(t, validationError.Reason, "failed to compile JSON schema") + assert.Contains(t, validationError.HowToFix, "complex regex patterns") + assert.Equal(t, "Query parameter 'failParam' failed schema compilation", validationError.Message) + found = true + break } } } diff --git a/requests/validate_request.go b/requests/validate_request.go index cb89624b..e2e38d2e 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -96,7 +96,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V SpecLine: 1, SpecCol: 0, HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + Context: input.Schema, }) return false, validationErrors } @@ -141,7 +141,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V SpecLine: 1, SpecCol: 0, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) return false, validationErrors } @@ -167,7 +167,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V SpecLine: line, SpecCol: col, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) return false, validationErrors } @@ -218,7 +218,6 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V violation := &errors.SchemaValidationFailure{ Reason: errMsg, - Location: er.InstanceLocation, // DEPRECATED FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), @@ -266,7 +265,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) } if len(validationErrors) > 0 { diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 9e7e75e0..c6cc8422 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -1488,16 +1488,13 @@ paths: found := false for _, err := range validationErrors { if err.ValidationSubType == helpers.Schema && - err.SchemaValidationErrors != nil && - len(err.SchemaValidationErrors) > 0 { - for _, schemaErr := range err.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" && - schemaErr.Reason != "" { - found = true - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, err.HowToFix, "complex regex patterns") - break - } + (err.SchemaValidationErrors == nil || len(err.SchemaValidationErrors) == 0) { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(err.Reason, "failed to compile JSON schema") { + found = true + assert.Contains(t, err.Reason, "failed to compile JSON schema") + assert.Contains(t, err.HowToFix, "complex regex patterns") + break } } } diff --git a/responses/validate_response.go b/responses/validate_response.go index b6633e64..4345879f 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -100,7 +100,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecLine: 1, SpecCol: 0, HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + Context: input.Schema, }) return false, validationErrors } @@ -132,7 +132,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecLine: 1, SpecCol: 0, HowToFix: "ensure response object has been set", - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) return false, validationErrors } @@ -149,7 +149,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecLine: 1, SpecCol: 0, HowToFix: "ensure body is not empty", - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) return false, validationErrors } @@ -173,7 +173,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecLine: 1, SpecCol: 0, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) return false, validationErrors } @@ -224,17 +224,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors referenceObject = string(responseBody) } - 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, + 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 { @@ -274,7 +273,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) } if len(validationErrors) > 0 { diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 9282bd5e..9bdb528d 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -75,11 +75,10 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo } if errMsg != "" { - // locate the violated property in the schema - located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation) + // 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), diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index b6104f62..c18a3cf7 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -115,12 +115,8 @@ func TestValidateDocument_SchemaCompilationFailure(t *testing.T) { assert.Equal(t, 1, validationError.SpecLine) assert.Equal(t, 0, validationError.SpecCol) - // Verify schema validation errors - assert.NotEmpty(t, validationError.SchemaValidationErrors) - schemaErr := validationError.SchemaValidationErrors[0] - assert.Equal(t, "schema compilation", schemaErr.Location) - assert.Contains(t, schemaErr.Reason, "failed to compile OpenAPI schema") - assert.Equal(t, malformedSchema, schemaErr.ReferenceSchema) + // Schema compilation errors don't have SchemaValidationFailure objects + assert.Empty(t, validationError.SchemaValidationErrors) } // TestValidateDocument_CompilationFailure tests the actual ValidateOpenAPIDocument function diff --git a/validator_test.go b/validator_test.go index 08b22f4d..c2392f3c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -313,7 +313,7 @@ paths: assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) require.Len(t, errors[0].SchemaValidationErrors, 1) require.NotNil(t, errors[0].SchemaValidationErrors[0]) - assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].KeywordLocation) assert.Equal(t, "'big mac' is not valid capital: expected first latter to be uppercase", errors[0].SchemaValidationErrors[0].Reason) } From 361e10a7f95d5778fff6aa0a690f0c18cbf0f470 Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 10:24:07 -0800 Subject: [PATCH 14/15] feat: add centralized JSON Pointer construction helpers Creates helper functions in helpers package for constructing RFC 6901-compliant JSON Pointer paths to OpenAPI specification locations. New functions: - EscapeJSONPointerSegment: Escapes ~ and / characters per RFC 6901 - ConstructParameterJSONPointer: Builds paths for parameter schemas - ConstructResponseHeaderJSONPointer: Builds paths for response headers This eliminates duplication of the escaping logic across 72+ locations in the codebase and provides a single source of truth for JSON Pointer construction. Pattern: Before: Manual escaping in each error function (3 lines of code each) After: Single function call with semantic naming Next step: Refactor all existing inline JSON Pointer construction to use these helpers. --- helpers/json_pointer.go | 40 ++++++++++ helpers/json_pointer_test.go | 150 +++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 helpers/json_pointer.go create mode 100644 helpers/json_pointer_test.go diff --git a/helpers/json_pointer.go b/helpers/json_pointer.go new file mode 100644 index 00000000..3ec390cc --- /dev/null +++ b/helpers/json_pointer.go @@ -0,0 +1,40 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package helpers + +import ( + "fmt" + "strings" +) + +// EscapeJSONPointerSegment escapes a single segment for use in a JSON Pointer (RFC 6901). +// It replaces '~' with '~0' and '/' with '~1'. +func EscapeJSONPointerSegment(segment string) string { + escaped := strings.ReplaceAll(segment, "~", "~0") + escaped = strings.ReplaceAll(escaped, "/", "~1") + return escaped +} + +// ConstructParameterJSONPointer constructs a full JSON Pointer path for a parameter +// in the OpenAPI specification. +// Format: /paths/{path}/{method}/parameters/{paramName}/schema/{keyword} +// The path segment is automatically escaped according to RFC 6901. +func ConstructParameterJSONPointer(pathTemplate, method, paramName, keyword string) string { + escapedPath := EscapeJSONPointerSegment(pathTemplate) + escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding + method = strings.ToLower(method) + return fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/%s", escapedPath, method, paramName, keyword) +} + +// ConstructResponseHeaderJSONPointer constructs a full JSON Pointer path for a response header +// in the OpenAPI specification. +// Format: /paths/{path}/{method}/responses/{statusCode}/headers/{headerName}/{keyword} +// The path segment is automatically escaped according to RFC 6901. +func ConstructResponseHeaderJSONPointer(pathTemplate, method, statusCode, headerName, keyword string) string { + escapedPath := EscapeJSONPointerSegment(pathTemplate) + escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding + method = strings.ToLower(method) + return fmt.Sprintf("/paths/%s/%s/responses/%s/headers/%s/%s", escapedPath, method, statusCode, headerName, keyword) +} + diff --git a/helpers/json_pointer_test.go b/helpers/json_pointer_test.go new file mode 100644 index 00000000..f96eb8ad --- /dev/null +++ b/helpers/json_pointer_test.go @@ -0,0 +1,150 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEscapeJSONPointerSegment(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no special characters", + input: "simple", + expected: "simple", + }, + { + name: "tilde only", + input: "some~thing", + expected: "some~0thing", + }, + { + name: "slash only", + input: "path/to/something", + expected: "path~1to~1something", + }, + { + name: "both tilde and slash", + input: "path/with~special/chars~", + expected: "path~1with~0special~1chars~0", + }, + { + name: "path template", + input: "/users/{id}", + expected: "~1users~1{id}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EscapeJSONPointerSegment(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestConstructParameterJSONPointer(t *testing.T) { + tests := []struct { + name string + pathTemplate string + method string + paramName string + keyword string + expected string + }{ + { + name: "simple path with query parameter type", + pathTemplate: "/users", + method: "GET", + paramName: "limit", + keyword: "type", + expected: "/paths/users/get/parameters/limit/schema/type", + }, + { + name: "path with parameter and enum keyword", + pathTemplate: "/users/{id}", + method: "POST", + paramName: "status", + keyword: "enum", + expected: "/paths/users~1{id}/post/parameters/status/schema/enum", + }, + { + name: "path with tilde character", + pathTemplate: "/some~path", + method: "PUT", + paramName: "value", + keyword: "format", + expected: "/paths/some~0path/put/parameters/value/schema/format", + }, + { + name: "path with multiple slashes", + pathTemplate: "/api/v1/users/{userId}/posts/{postId}", + method: "DELETE", + paramName: "filter", + keyword: "required", + expected: "/paths/api~1v1~1users~1{userId}~1posts~1{postId}/delete/parameters/filter/schema/required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConstructParameterJSONPointer(tt.pathTemplate, tt.method, tt.paramName, tt.keyword) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestConstructResponseHeaderJSONPointer(t *testing.T) { + tests := []struct { + name string + pathTemplate string + method string + statusCode string + headerName string + keyword string + expected string + }{ + { + name: "simple response header", + pathTemplate: "/health", + method: "GET", + statusCode: "200", + headerName: "X-Request-ID", + keyword: "required", + expected: "/paths/health/get/responses/200/headers/X-Request-ID/required", + }, + { + name: "path with parameter", + pathTemplate: "/users/{id}", + method: "POST", + statusCode: "201", + headerName: "Location", + keyword: "schema", + expected: "/paths/users~1{id}/post/responses/201/headers/Location/schema", + }, + { + name: "path with tilde and slash", + pathTemplate: "/some~path/to/resource", + method: "PUT", + statusCode: "204", + headerName: "ETag", + keyword: "type", + expected: "/paths/some~0path~1to~1resource/put/responses/204/headers/ETag/type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConstructResponseHeaderJSONPointer(tt.pathTemplate, tt.method, tt.statusCode, tt.headerName, tt.keyword) + assert.Equal(t, tt.expected, result) + }) + } +} + From 993831ae9f91f5a7c7fb0f22fb7048540ddb908f Mon Sep 17 00:00:00 2001 From: Michael Bonifacio Date: Sat, 15 Nov 2025 10:27:05 -0800 Subject: [PATCH 15/15] refactor: use centralized JSON Pointer helpers across codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces all manual JSON Pointer construction with calls to the new helper functions, eliminating 72+ instances of duplicated escaping logic. Changes: - errors/parameter_errors.go: Replaced all manual escaping with helpers.ConstructParameterJSONPointer() calls - All 35 parameter error functions now use helper - Handles type, enum, items/type, items/enum, maxItems, minItems, uniqueItems - responses/validate_headers.go: Replaced manual escaping with helpers.ConstructResponseHeaderJSONPointer() - errors/validation_error_test.go: Updated tests to use FieldPath instead of deprecated Location field Benefits: - Single source of truth for JSON Pointer construction - Reduced code duplication (3 lines → 1 line per usage) - More maintainable and less error-prone - Semantic function names make intent clearer Each function call reduced from: escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") escapedPath = strings.TrimPrefix(escapedPath, "~1") keywordLocation := fmt.Sprintf("/paths/%s/%s/...", escapedPath, ...) To: keywordLocation := helpers.ConstructParameterJSONPointer(path, method, param, keyword) --- errors/parameter_errors.go | 125 +++++++------------------------- errors/validation_error_test.go | 14 ++-- helpers/json_pointer.go | 1 + responses/validate_headers.go | 8 +- 4 files changed, 34 insertions(+), 114 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 9bbcee85..1c298fd7 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -77,10 +77,7 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE } func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/required", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -129,10 +126,7 @@ func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation } func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -160,10 +154,7 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, } validEnums := strings.Join(enums, ", ") - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -188,10 +179,7 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, func IncorrectQueryParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -214,10 +202,7 @@ func IncorrectQueryParamArrayBoolean( } func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/maxItems", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "maxItems") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -240,10 +225,7 @@ func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expec } func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/minItems", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "minItems") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -266,10 +248,7 @@ func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expec } func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/uniqueItems", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "uniqueItems") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -293,10 +272,7 @@ func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, dupli func IncorrectCookieParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -321,10 +297,7 @@ func IncorrectCookieParamArrayBoolean( func IncorrectQueryParamArrayInteger( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -349,10 +322,7 @@ func IncorrectQueryParamArrayInteger( func IncorrectQueryParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -377,10 +347,7 @@ func IncorrectQueryParamArrayNumber( func IncorrectCookieParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -429,10 +396,7 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema } func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -456,10 +420,7 @@ func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, p } func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -483,10 +444,7 @@ func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, } func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -516,10 +474,7 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, p } validEnums := strings.Join(enums, ", ") - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -551,10 +506,7 @@ func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Sche } validEnums := strings.Join(enums, ", ") - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/enum", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/enum") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -603,10 +555,7 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, p } func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -630,10 +579,7 @@ func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, } func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -657,10 +603,7 @@ func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, } func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -683,10 +626,7 @@ func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, } func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -709,10 +649,7 @@ func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, } func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -736,10 +673,7 @@ func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, } func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -768,10 +702,7 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, } validEnums := strings.Join(enums, ", ") - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/enum", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -796,10 +727,7 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, func IncorrectHeaderParamArrayBoolean( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -824,10 +752,7 @@ func IncorrectHeaderParamArrayBoolean( func IncorrectHeaderParamArrayNumber( param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - escapedPath = strings.TrimPrefix(escapedPath, "~1") - keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/items/type", escapedPath, strings.ToLower(operation), param.Name) + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") return &ValidationError{ ValidationType: helpers.ParameterValidation, diff --git a/errors/validation_error_test.go b/errors/validation_error_test.go index 749dedfd..c68ca4cf 100644 --- a/errors/validation_error_test.go +++ b/errors/validation_error_test.go @@ -13,11 +13,11 @@ import ( func TestSchemaValidationFailure_Error(t *testing.T) { // Test the Error method of SchemaValidationFailure s := &SchemaValidationFailure{ - Reason: "Invalid type", - Location: "/path/to/property", + Reason: "Invalid type", + FieldPath: "$.path.to.property", } - expectedError := "Reason: Invalid type, Location: /path/to/property" + expectedError := "Reason: Invalid type, FieldPath: $.path.to.property" require.Equal(t, expectedError, s.Error()) } @@ -48,8 +48,8 @@ func TestValidationError_Error_WithSpecLineAndColumn(t *testing.T) { func TestValidationError_Error_WithSchemaValidationErrors(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors schemaError := &SchemaValidationFailure{ - Reason: "Invalid enum value", - Location: "/path/to/enum", + Reason: "Invalid enum value", + FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", @@ -64,8 +64,8 @@ func TestValidationError_Error_WithSchemaValidationErrors(t *testing.T) { func TestValidationError_Error_WithSchemaValidationErrors_AndSpecLineColumn(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors and SpecLine and SpecCol schemaError := &SchemaValidationFailure{ - Reason: "Invalid enum value", - Location: "/path/to/enum", + Reason: "Invalid enum value", + FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", diff --git a/helpers/json_pointer.go b/helpers/json_pointer.go index 3ec390cc..a7bccac9 100644 --- a/helpers/json_pointer.go +++ b/helpers/json_pointer.go @@ -20,6 +20,7 @@ func EscapeJSONPointerSegment(segment string) string { // in the OpenAPI specification. // Format: /paths/{path}/{method}/parameters/{paramName}/schema/{keyword} // The path segment is automatically escaped according to RFC 6901. +// The keyword can be a simple keyword like "type" or a nested path like "items/type". func ConstructParameterJSONPointer(pathTemplate, method, paramName, keyword string) string { escapedPath := EscapeJSONPointerSegment(pathTemplate) escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding diff --git a/responses/validate_headers.go b/responses/validate_headers.go index 2d5499ca..8546781b 100644 --- a/responses/validate_headers.go +++ b/responses/validate_headers.go @@ -55,13 +55,7 @@ func ValidateResponseHeaders( for name, header := range headers.FromOldest() { if header.Required { if _, ok := locatedHeaders[strings.ToLower(name)]; !ok { - // Construct full OpenAPI path for KeywordLocation - // e.g., /paths/~1health/get/responses/200/headers/chicken-nuggets/required - escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") - escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") - method := strings.ToLower(request.Method) - keywordLocation := fmt.Sprintf("/paths/%s/%s/responses/%s/headers/%s/required", - escapedPath, method, statusCode, name) + keywordLocation := helpers.ConstructResponseHeaderJSONPointer(pathTemplate, request.Method, statusCode, name, "required") validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation,