Skip to content

Commit 37c622d

Browse files
committed
added logging to check for empty schemas
improved schema location value by adding multiple locations for each schema error. Signed-off-by: Dave Shanley <[email protected]>
1 parent 05d080c commit 37c622d

File tree

9 files changed

+1078
-1013
lines changed

9 files changed

+1078
-1013
lines changed

errors/validation_error.go

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,89 @@
44
package errors
55

66
import (
7-
"fmt"
8-
"github.com/santhosh-tekuri/jsonschema/v5"
7+
"fmt"
8+
"github.com/santhosh-tekuri/jsonschema/v5"
99
)
1010

1111
// SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more
1212
// user-friendly way to break down what went wrong.
1313
type SchemaValidationFailure struct {
14-
// Reason is a human-readable message describing the reason for the error.
15-
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
14+
// Reason is a human-readable message describing the reason for the error.
15+
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
1616

17-
// Location is the XPath-like location of the validation failure
18-
Location string `json:"location,omitempty" yaml:"location,omitempty"`
17+
// Location is the XPath-like location of the validation failure
18+
Location string `json:"location,omitempty" yaml:"location,omitempty"`
1919

20-
// Line is the line number where the violation occurred. This may a local line number
21-
// if the validation is a schema (only schemas are validated locally, so the line number will be relative to
22-
// the Context object held by the ValidationError object).
23-
Line int `json:"line,omitempty" yaml:"line,omitempty"`
20+
// DeepLocation is the path to the validation failure as exposed by the jsonschema library.
21+
DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"`
2422

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

30-
// The original error object, which is a jsonschema.ValidationError object.
31-
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
26+
// Line is the line number where the violation occurred. This may a local line number
27+
// if the validation is a schema (only schemas are validated locally, so the line number will be relative to
28+
// the Context object held by the ValidationError object).
29+
Line int `json:"line,omitempty" yaml:"line,omitempty"`
30+
31+
// Column is the column number where the violation occurred. This may a local column number
32+
// if the validation is a schema (only schemas are validated locally, so the column number will be relative to
33+
// the Context object held by the ValidationError object).
34+
Column int `json:"column,omitempty" yaml:"column,omitempty"`
35+
36+
// The original error object, which is a jsonschema.ValidationError object.
37+
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
3238
}
3339

3440
// Error returns a string representation of the error
3541
func (s *SchemaValidationFailure) Error() string {
36-
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
42+
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
3743
}
3844

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

42-
// Message is a human-readable message describing the error.
43-
Message string `json:"message" yaml:"message"`
48+
// Message is a human-readable message describing the error.
49+
Message string `json:"message" yaml:"message"`
4450

45-
// Reason is a human-readable message describing the reason for the error.
46-
Reason string `json:"reason" yaml:"reason"`
51+
// Reason is a human-readable message describing the reason for the error.
52+
Reason string `json:"reason" yaml:"reason"`
4753

48-
// ValidationType is a string that describes the type of validation that failed.
49-
ValidationType string `json:"validationType" yaml:"validationType"`
54+
// ValidationType is a string that describes the type of validation that failed.
55+
ValidationType string `json:"validationType" yaml:"validationType"`
5056

51-
// ValidationSubType is a string that describes the subtype of validation that failed.
52-
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`
57+
// ValidationSubType is a string that describes the subtype of validation that failed.
58+
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`
5359

54-
// SpecLine is the line number in the spec where the error occurred.
55-
SpecLine int `json:"specLine" yaml:"specLine"`
60+
// SpecLine is the line number in the spec where the error occurred.
61+
SpecLine int `json:"specLine" yaml:"specLine"`
5662

57-
// SpecCol is the column number in the spec where the error occurred.
58-
SpecCol int `json:"specColumn" yaml:"specColumn"`
63+
// SpecCol is the column number in the spec where the error occurred.
64+
SpecCol int `json:"specColumn" yaml:"specColumn"`
5965

60-
// HowToFix is a human-readable message describing how to fix the error.
61-
HowToFix string `json:"howToFix" yaml:"howToFix"`
66+
// HowToFix is a human-readable message describing how to fix the error.
67+
HowToFix string `json:"howToFix" yaml:"howToFix"`
6268

63-
// SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
64-
// This is only populated whe the validation type is against a schema.
65-
SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"`
69+
// SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
70+
// This is only populated whe the validation type is against a schema.
71+
SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"`
6672

67-
// Context is the object that the validation error occurred on. This is usually a pointer to a schema
68-
// or a parameter object.
69-
Context interface{} `json:"-" yaml:"-"`
73+
// Context is the object that the validation error occurred on. This is usually a pointer to a schema
74+
// or a parameter object.
75+
Context interface{} `json:"-" yaml:"-"`
7076
}
7177

7278
// Error returns a string representation of the error
7379
func (v *ValidationError) Error() string {
74-
if v.SchemaValidationErrors != nil {
75-
return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d",
76-
v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol)
77-
} else {
78-
return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d",
79-
v.Message, v.Reason, v.SpecLine, v.SpecCol)
80-
}
80+
if v.SchemaValidationErrors != nil {
81+
return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d",
82+
v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol)
83+
} else {
84+
return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d",
85+
v.Message, v.Reason, v.SpecLine, v.SpecCol)
86+
}
8187
}
8288

8389
// IsPathMissingError returns true if the error has a ValidationType of "path" and a ValidationSubType of "missing"
8490
func (v *ValidationError) IsPathMissingError() bool {
85-
return v.ValidationType == "path" && v.ValidationSubType == "missing"
91+
return v.ValidationType == "path" && v.ValidationSubType == "missing"
8692
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ require (
88
github.com/stretchr/testify v1.8.0
99
github.com/vmware-labs/yaml-jsonpath v0.3.2
1010
gopkg.in/yaml.v3 v3.0.1
11+
go.uber.org/zap v1.24.0
1112
)
1213

1314
require (
1415
github.com/davecgh/go-spew v1.1.1 // indirect
1516
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
1617
github.com/pmezard/go-difflib v1.0.0 // indirect
18+
go.uber.org/atomic v1.11.0 // indirect
19+
go.uber.org/multierr v1.11.0 // indirect
20+
1721
golang.org/x/sync v0.1.0 // indirect
1822
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
6969
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
7070
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
7171
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
72+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
73+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
74+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
75+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
76+
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
77+
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
7278
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7379
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
7480
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

schema_validation/validate_document.go

Lines changed: 63 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,81 +4,83 @@
44
package schema_validation
55

66
import (
7-
"fmt"
8-
"github.com/pb33f/libopenapi"
9-
"github.com/pb33f/libopenapi-validator/errors"
10-
"github.com/pb33f/libopenapi-validator/helpers"
11-
"github.com/santhosh-tekuri/jsonschema/v5"
12-
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
13-
"strings"
7+
"fmt"
8+
"github.com/pb33f/libopenapi"
9+
"github.com/pb33f/libopenapi-validator/errors"
10+
"github.com/pb33f/libopenapi-validator/helpers"
11+
"github.com/santhosh-tekuri/jsonschema/v5"
12+
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
13+
"strings"
1414
)
1515

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

20-
info := doc.GetSpecInfo()
21-
loadedSchema := info.APISchema
22-
var validationErrors []*errors.ValidationError
23-
decodedDocument := *info.SpecJSON
20+
info := doc.GetSpecInfo()
21+
loadedSchema := info.APISchema
22+
var validationErrors []*errors.ValidationError
23+
decodedDocument := *info.SpecJSON
2424

25-
compiler := jsonschema.NewCompiler()
26-
_ = compiler.AddResource("schema.json", strings.NewReader(loadedSchema))
27-
jsch, _ := compiler.Compile("schema.json")
25+
compiler := jsonschema.NewCompiler()
26+
_ = compiler.AddResource("schema.json", strings.NewReader(loadedSchema))
27+
jsch, _ := compiler.Compile("schema.json")
2828

29-
scErrs := jsch.Validate(decodedDocument)
29+
scErrs := jsch.Validate(decodedDocument)
3030

31-
var schemaValidationErrors []*errors.SchemaValidationFailure
31+
var schemaValidationErrors []*errors.SchemaValidationFailure
3232

33-
if scErrs != nil {
33+
if scErrs != nil {
3434

35-
if jk, ok := scErrs.(*jsonschema.ValidationError); ok {
35+
if jk, ok := scErrs.(*jsonschema.ValidationError); ok {
3636

37-
// flatten the validationErrors
38-
schFlatErrs := jk.BasicOutput().Errors
37+
// flatten the validationErrors
38+
schFlatErrs := jk.BasicOutput().Errors
3939

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

47-
// locate the violated property in the schema
47+
// locate the violated property in the schema
4848

49-
located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.KeywordLocation)
50-
if located == nil {
51-
// try again with the instance location
52-
located = LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation)
53-
}
54-
violation := &errors.SchemaValidationFailure{
55-
Reason: er.Error,
56-
Location: er.KeywordLocation,
57-
OriginalError: jk,
58-
}
59-
// if we have a location within the schema, add it to the error
60-
if located != nil {
61-
// location of the violation within the rendered schema.
62-
violation.Line = located.Line
63-
violation.Column = located.Column
64-
}
65-
schemaValidationErrors = append(schemaValidationErrors, violation)
66-
}
67-
}
68-
}
49+
located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.KeywordLocation)
50+
if located == nil {
51+
// try again with the instance location
52+
located = LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation)
53+
}
54+
violation := &errors.SchemaValidationFailure{
55+
Reason: er.Error,
56+
Location: er.InstanceLocation,
57+
DeepLocation: er.KeywordLocation,
58+
AbsoluteLocation: er.AbsoluteKeywordLocation,
59+
OriginalError: jk,
60+
}
61+
// if we have a location within the schema, add it to the error
62+
if located != nil {
63+
// location of the violation within the rendered schema.
64+
violation.Line = located.Line
65+
violation.Column = located.Column
66+
}
67+
schemaValidationErrors = append(schemaValidationErrors, violation)
68+
}
69+
}
70+
}
6971

70-
// add the error to the list
71-
validationErrors = append(validationErrors, &errors.ValidationError{
72-
ValidationType: helpers.Schema,
73-
Message: "Document does not pass validation",
74-
Reason: fmt.Sprintf("OpenAPI document is not valid according "+
75-
"to the %s specification", info.Version),
76-
SchemaValidationErrors: schemaValidationErrors,
77-
HowToFix: errors.HowToFixInvalidSchema,
78-
})
79-
}
80-
if len(validationErrors) > 0 {
81-
return false, validationErrors
82-
}
83-
return true, nil
72+
// add the error to the list
73+
validationErrors = append(validationErrors, &errors.ValidationError{
74+
ValidationType: helpers.Schema,
75+
Message: "Document does not pass validation",
76+
Reason: fmt.Sprintf("OpenAPI document is not valid according "+
77+
"to the %s specification", info.Version),
78+
SchemaValidationErrors: schemaValidationErrors,
79+
HowToFix: errors.HowToFixInvalidSchema,
80+
})
81+
}
82+
if len(validationErrors) > 0 {
83+
return false, validationErrors
84+
}
85+
return true, nil
8486
}

0 commit comments

Comments
 (0)