Skip to content

Commit

Permalink
added logging to check for empty schemas
Browse files Browse the repository at this point in the history
improved schema location value by adding multiple locations for each schema error.

Signed-off-by: Dave Shanley <dave@quobix.com>
  • Loading branch information
daveshanley committed Jul 16, 2023
1 parent 05d080c commit b9b6477
Show file tree
Hide file tree
Showing 9 changed files with 1,078 additions and 1,013 deletions.
96 changes: 51 additions & 45 deletions errors/validation_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,89 @@
package errors

import (
"fmt"
"github.com/santhosh-tekuri/jsonschema/v5"
"fmt"
"github.com/santhosh-tekuri/jsonschema/v5"
)

// SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more
// user-friendly way to break down what went wrong.
type SchemaValidationFailure struct {
// Reason is a human-readable message describing the reason for the error.
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
// 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"`
// Location is the XPath-like location of the validation failure
Location string `json:"location,omitempty" yaml:"location,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).
Line int `json:"line,omitempty" yaml:"line,omitempty"`
// DeepLocation is the path to the validation failure as exposed by the jsonschema library.
DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"`

// Column is the column number where the violation occurred. This may a local column number
// if the validation is a schema (only schemas are validated locally, so the column number will be relative to
// the Context object held by the ValidationError object).
Column int `json:"column,omitempty" yaml:"column,omitempty"`
// AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library.
AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"`

// The original error object, which is a jsonschema.ValidationError object.
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
// 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).
Line int `json:"line,omitempty" yaml:"line,omitempty"`

// Column is the column number where the violation occurred. This may a local column number
// if the validation is a schema (only schemas are validated locally, so the column number will be relative to
// the Context object held by the ValidationError object).
Column int `json:"column,omitempty" yaml:"column,omitempty"`

// The original error object, which is a jsonschema.ValidationError object.
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
}

// Error returns a string representation of the error
func (s *SchemaValidationFailure) Error() string {
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
}

// ValidationError is a struct that contains all the information about a validation error.
type ValidationError struct {

// Message is a human-readable message describing the error.
Message string `json:"message" yaml:"message"`
// Message is a human-readable message describing the error.
Message string `json:"message" yaml:"message"`

// Reason is a human-readable message describing the reason for the error.
Reason string `json:"reason" yaml:"reason"`
// Reason is a human-readable message describing the reason for the error.
Reason string `json:"reason" yaml:"reason"`

// ValidationType is a string that describes the type of validation that failed.
ValidationType string `json:"validationType" yaml:"validationType"`
// ValidationType is a string that describes the type of validation that failed.
ValidationType string `json:"validationType" yaml:"validationType"`

// ValidationSubType is a string that describes the subtype of validation that failed.
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`
// ValidationSubType is a string that describes the subtype of validation that failed.
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`

// SpecLine is the line number in the spec where the error occurred.
SpecLine int `json:"specLine" yaml:"specLine"`
// SpecLine is the line number in the spec where the error occurred.
SpecLine int `json:"specLine" yaml:"specLine"`

// SpecCol is the column number in the spec where the error occurred.
SpecCol int `json:"specColumn" yaml:"specColumn"`
// SpecCol is the column number in the spec where the error occurred.
SpecCol int `json:"specColumn" yaml:"specColumn"`

// HowToFix is a human-readable message describing how to fix the error.
HowToFix string `json:"howToFix" yaml:"howToFix"`
// HowToFix is a human-readable message describing how to fix the error.
HowToFix string `json:"howToFix" yaml:"howToFix"`

// SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
// This is only populated whe the validation type is against a schema.
SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,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.
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
// or a parameter object.
Context interface{} `json:"-" yaml:"-"`
// Context is the object that the validation error occurred on. This is usually a pointer to a schema
// or a parameter object.
Context interface{} `json:"-" yaml:"-"`
}

// Error returns a string representation of the error
func (v *ValidationError) Error() string {
if v.SchemaValidationErrors != nil {
return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d",
v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol)
} else {
return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d",
v.Message, v.Reason, v.SpecLine, v.SpecCol)
}
if v.SchemaValidationErrors != nil {
return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d",
v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol)
} else {
return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d",
v.Message, v.Reason, v.SpecLine, v.SpecCol)
}
}

// IsPathMissingError returns true if the error has a ValidationType of "path" and a ValidationSubType of "missing"
func (v *ValidationError) IsPathMissingError() bool {
return v.ValidationType == "path" && v.ValidationSubType == "missing"
return v.ValidationType == "path" && v.ValidationSubType == "missing"
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ require (
github.com/stretchr/testify v1.8.0
github.com/vmware-labs/yaml-jsonpath v0.3.2
gopkg.in/yaml.v3 v3.0.1
go.uber.org/zap v1.24.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect

golang.org/x/sync v0.1.0 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down
124 changes: 63 additions & 61 deletions schema_validation/validate_document.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,83 @@
package schema_validation

import (
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"strings"
"fmt"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"strings"
)

// ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version)
// It will return true if the document is valid, false if it is not and a slice of ValidationError pointers.
func ValidateOpenAPIDocument(doc libopenapi.Document) (bool, []*errors.ValidationError) {

info := doc.GetSpecInfo()
loadedSchema := info.APISchema
var validationErrors []*errors.ValidationError
decodedDocument := *info.SpecJSON
info := doc.GetSpecInfo()
loadedSchema := info.APISchema
var validationErrors []*errors.ValidationError
decodedDocument := *info.SpecJSON

compiler := jsonschema.NewCompiler()
_ = compiler.AddResource("schema.json", strings.NewReader(loadedSchema))
jsch, _ := compiler.Compile("schema.json")
compiler := jsonschema.NewCompiler()
_ = compiler.AddResource("schema.json", strings.NewReader(loadedSchema))
jsch, _ := compiler.Compile("schema.json")

scErrs := jsch.Validate(decodedDocument)
scErrs := jsch.Validate(decodedDocument)

var schemaValidationErrors []*errors.SchemaValidationFailure
var schemaValidationErrors []*errors.SchemaValidationFailure

if scErrs != nil {
if scErrs != nil {

if jk, ok := scErrs.(*jsonschema.ValidationError); ok {
if jk, ok := scErrs.(*jsonschema.ValidationError); ok {

// flatten the validationErrors
schFlatErrs := jk.BasicOutput().Errors
// flatten the validationErrors
schFlatErrs := jk.BasicOutput().Errors

for q := range schFlatErrs {
er := schFlatErrs[q]
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
continue // ignore this error, it's useless tbh, utter noise.
}
if er.Error != "" {
for q := range schFlatErrs {
er := schFlatErrs[q]
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
continue // ignore this error, it's useless tbh, utter noise.
}
if er.Error != "" {

// locate the violated property in the schema
// locate the violated property in the schema

located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.KeywordLocation)
if located == nil {
// try again with the instance location
located = LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation)
}
violation := &errors.SchemaValidationFailure{
Reason: er.Error,
Location: er.KeywordLocation,
OriginalError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {
// location of the violation within the rendered schema.
violation.Line = located.Line
violation.Column = located.Column
}
schemaValidationErrors = append(schemaValidationErrors, violation)
}
}
}
located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.KeywordLocation)
if located == nil {
// try again with the instance location
located = LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation)
}
violation := &errors.SchemaValidationFailure{
Reason: er.Error,
Location: er.InstanceLocation,
DeepLocation: er.KeywordLocation,
AbsoluteLocation: er.AbsoluteKeywordLocation,
OriginalError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {
// location of the violation within the rendered schema.
violation.Line = located.Line
violation.Column = located.Column
}
schemaValidationErrors = append(schemaValidationErrors, violation)
}
}
}

// add the error to the list
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.Schema,
Message: "Document does not pass validation",
Reason: fmt.Sprintf("OpenAPI document is not valid according "+
"to the %s specification", info.Version),
SchemaValidationErrors: schemaValidationErrors,
HowToFix: errors.HowToFixInvalidSchema,
})
}
if len(validationErrors) > 0 {
return false, validationErrors
}
return true, nil
// add the error to the list
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.Schema,
Message: "Document does not pass validation",
Reason: fmt.Sprintf("OpenAPI document is not valid according "+
"to the %s specification", info.Version),
SchemaValidationErrors: schemaValidationErrors,
HowToFix: errors.HowToFixInvalidSchema,
})
}
if len(validationErrors) > 0 {
return false, validationErrors
}
return true, nil
}
Loading

0 comments on commit b9b6477

Please sign in to comment.