Skip to content
Draft
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Working memory and debug artifacts
working-memory/
34 changes: 17 additions & 17 deletions errors/validation_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,25 @@ import (
"github.com/santhosh-tekuri/jsonschema/v6"
)

// SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more
// user-friendly way to break down what went wrong.
// SchemaValidationFailure describes any failure that occurs when validating data
// against either an OpenAPI or JSON Schema. It aims to be a more user-friendly
// representation of the error than what is provided by the jsonschema library.
type SchemaValidationFailure struct {
// Reason is a human-readable message describing the reason for the error.
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`

// Location is the XPath-like location of the validation failure
Location string `json:"location,omitempty" yaml:"location,omitempty"`
// InstancePath is the raw path segments from the root to the failing field
InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"`

// FieldName is the name of the specific field that failed validation (last segment of the path)
FieldName string `json:"fieldName,omitempty" yaml:"fieldName,omitempty"`

// FieldPath is the JSONPath representation of the field location (e.g., "$.user.email")
// FieldPath is the JSONPath representation of the field location that failed validation (e.g., "$.user.email")
FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"`

// InstancePath is the raw path segments from the root to the failing field
InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"`

// DeepLocation is the path to the validation failure as exposed by the jsonschema library.
DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"`

// AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library.
AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"`
// KeywordLocation is the JSON Pointer (RFC 6901) path to the schema keyword that failed validation
// (e.g., "/properties/age/minimum")
KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"`

// Line is the line number where the violation occurred. This may a local line number
// if the validation is a schema (only schemas are validated locally, so the line number will be relative to
Expand All @@ -46,14 +42,18 @@ type SchemaValidationFailure struct {
// ReferenceSchema is the schema that was referenced in the validation failure.
ReferenceSchema string `json:"referenceSchema,omitempty" yaml:"referenceSchema,omitempty"`

// ReferenceObject is the object that was referenced in the validation failure.
// ReferenceObject is the object that failed schema validation
ReferenceObject string `json:"referenceObject,omitempty" yaml:"referenceObject,omitempty"`

// ReferenceExample is an example object generated from the schema that was referenced in the validation failure.
ReferenceExample string `json:"referenceExample,omitempty" yaml:"referenceExample,omitempty"`

// The original error object, which is a jsonschema.ValidationError object.
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
// The original jsonschema.ValidationError object, if the schema failure originated from the jsonschema library.
OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"`

// DEPRECATED in favor of explicit use of FieldPath & InstancePath
// Location is the XPath-like location of the validation failure
Location string `json:"location,omitempty" yaml:"location,omitempty"`
}

// Error returns a string representation of the error
Expand Down Expand Up @@ -97,7 +97,7 @@ type ValidationError struct {
ParameterName string `json:"parameterName,omitempty" yaml:"parameterName,omitempty"`

// SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
// This is only populated whe the validation type is against a schema.
// This is only populated when the validation type is against a schema.
SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"`

// Context is the object that the validation error occurred on. This is usually a pointer to a schema
Expand Down
29 changes: 12 additions & 17 deletions parameters/validate_parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -226,12 +220,13 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val
}

fail := &errors.SchemaValidationFailure{
Reason: errMsg,
Location: er.KeywordLocation,
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
OriginalError: scErrs,
Reason: errMsg,
Location: er.KeywordLocation, // DEPRECATED
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
KeywordLocation: er.KeywordLocation,
OriginalJsonSchemaError: scErrs,
}
if schema != nil {
rendered, err := schema.RenderInline()
Expand Down
4 changes: 2 additions & 2 deletions requests/validate_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
66 changes: 24 additions & 42 deletions requests/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -236,14 +217,15 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V
errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{}))

violation := &errors.SchemaValidationFailure{
Reason: errMsg,
Location: er.KeywordLocation,
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
ReferenceSchema: referenceSchema,
ReferenceObject: referenceObject,
OriginalError: jk,
Reason: errMsg,
Location: er.InstanceLocation, // DEPRECATED
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
KeywordLocation: er.KeywordLocation,
ReferenceSchema: referenceSchema,
ReferenceObject: referenceObject,
OriginalJsonSchemaError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {
Expand Down
3 changes: 2 additions & 1 deletion responses/validate_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
81 changes: 28 additions & 53 deletions responses/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,17 @@ 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,
Message: fmt.Sprintf("%d response body for '%s' failed schema compilation",
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
}
Expand All @@ -129,46 +123,33 @@ 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
}

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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -251,14 +225,15 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors
}

violation := &errors.SchemaValidationFailure{
Reason: errMsg,
Location: er.KeywordLocation,
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
ReferenceSchema: referenceSchema,
ReferenceObject: referenceObject,
OriginalError: jk,
Reason: errMsg,
Location: er.InstanceLocation, // DEPRECATED
FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation),
FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation),
InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation),
KeywordLocation: er.KeywordLocation,
ReferenceSchema: referenceSchema,
ReferenceObject: referenceObject,
OriginalJsonSchemaError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {
Expand Down
Loading