Skip to content

Commit a052a86

Browse files
committed
Adding new schema violation data, will be super useful downstream.
Signed-off-by: Dave Shanley <[email protected]>
1 parent a3e5c9f commit a052a86

File tree

4 files changed

+133
-55
lines changed

4 files changed

+133
-55
lines changed

errors/validation_error.go

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,89 +4,95 @@
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-
// DeepLocation is the path to the validation failure as exposed by the jsonschema library.
21-
DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,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"`
2222

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"`
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"`
2525

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"`
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"`
3030

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"`
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"`
3535

36-
// The original error object, which is a jsonschema.ValidationError object.
37-
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
36+
// ReferenceSchema is the schema that was referenced in the validation failure.
37+
ReferenceSchema string `json:"referenceSchema,omitempty" yaml:"referenceSchema,omitempty"`
38+
39+
// ReferenceObject is the object that was referenced in the validation failure.
40+
ReferenceObject string `json:"referenceObject,omitempty" yaml:"referenceObject,omitempty"`
41+
42+
// The original error object, which is a jsonschema.ValidationError object.
43+
OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"`
3844
}
3945

4046
// Error returns a string representation of the error
4147
func (s *SchemaValidationFailure) Error() string {
42-
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
48+
return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location)
4349
}
4450

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

48-
// Message is a human-readable message describing the error.
49-
Message string `json:"message" yaml:"message"`
54+
// Message is a human-readable message describing the error.
55+
Message string `json:"message" yaml:"message"`
5056

51-
// Reason is a human-readable message describing the reason for the error.
52-
Reason string `json:"reason" yaml:"reason"`
57+
// Reason is a human-readable message describing the reason for the error.
58+
Reason string `json:"reason" yaml:"reason"`
5359

54-
// ValidationType is a string that describes the type of validation that failed.
55-
ValidationType string `json:"validationType" yaml:"validationType"`
60+
// ValidationType is a string that describes the type of validation that failed.
61+
ValidationType string `json:"validationType" yaml:"validationType"`
5662

57-
// ValidationSubType is a string that describes the subtype of validation that failed.
58-
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`
63+
// ValidationSubType is a string that describes the subtype of validation that failed.
64+
ValidationSubType string `json:"validationSubType" yaml:"validationSubType"`
5965

60-
// SpecLine is the line number in the spec where the error occurred.
61-
SpecLine int `json:"specLine" yaml:"specLine"`
66+
// SpecLine is the line number in the spec where the error occurred.
67+
SpecLine int `json:"specLine" yaml:"specLine"`
6268

63-
// SpecCol is the column number in the spec where the error occurred.
64-
SpecCol int `json:"specColumn" yaml:"specColumn"`
69+
// SpecCol is the column number in the spec where the error occurred.
70+
SpecCol int `json:"specColumn" yaml:"specColumn"`
6571

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

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"`
75+
// SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
76+
// This is only populated whe the validation type is against a schema.
77+
SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"`
7278

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:"-"`
79+
// Context is the object that the validation error occurred on. This is usually a pointer to a schema
80+
// or a parameter object.
81+
Context interface{} `json:"-" yaml:"-"`
7682
}
7783

7884
// Error returns a string representation of the error
7985
func (v *ValidationError) Error() string {
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-
}
86+
if v.SchemaValidationErrors != nil {
87+
return fmt.Sprintf("Error: %s, Reason: %s, Validation Errors: %s, Line: %d, Column: %d",
88+
v.Message, v.Reason, v.SchemaValidationErrors, v.SpecLine, v.SpecCol)
89+
} else {
90+
return fmt.Sprintf("Error: %s, Reason: %s, Line: %d, Column: %d",
91+
v.Message, v.Reason, v.SpecLine, v.SpecCol)
92+
}
8793
}
8894

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

requests/validate_request.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import (
1515
"gopkg.in/yaml.v3"
1616
"io"
1717
"net/http"
18+
"reflect"
19+
"regexp"
20+
"strconv"
1821
"strings"
1922
)
2023

24+
var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
25+
2126
// ValidateRequestSchema will validate an http.Request pointer against a schema.
2227
// If validation fails, it will return a list of validation errors as the second return value.
2328
func ValidateRequestSchema(
@@ -67,10 +72,29 @@ func ValidateRequestSchema(
6772

6873
// locate the violated property in the schema
6974
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)
75+
76+
// extract the element specified by the instance
77+
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
78+
var referenceObject string
79+
80+
if len(val) > 0 {
81+
referenceIndex, _ := strconv.Atoi(val[1])
82+
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
83+
found := decodedObj.([]any)[referenceIndex]
84+
recoded, _ := json.MarshalIndent(found, "", " ")
85+
referenceObject = string(recoded)
86+
}
87+
}
88+
if referenceObject == "" {
89+
referenceObject = string(requestBody)
90+
}
91+
7092
violation := &errors.SchemaValidationFailure{
71-
Reason: er.Error,
72-
Location: er.KeywordLocation,
73-
OriginalError: jk,
93+
Reason: er.Error,
94+
Location: er.KeywordLocation,
95+
ReferenceSchema: string(renderedSchema),
96+
ReferenceObject: referenceObject,
97+
OriginalError: jk,
7498
}
7599
// if we have a location within the schema, add it to the error
76100
if located != nil {

responses/validate_response.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import (
1515
"gopkg.in/yaml.v3"
1616
"io"
1717
"net/http"
18+
"reflect"
19+
"regexp"
20+
"strconv"
1821
"strings"
1922
)
2023

24+
var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
25+
2126
// ValidateResponseSchema will validate the response body for a http.Response pointer. The request is used to
2227
// locate the operation in the specification, the response is used to ensure the response code, media type and the
2328
// schema of the response body are valid.
@@ -74,10 +79,29 @@ func ValidateResponseSchema(
7479

7580
// locate the violated property in the schema
7681
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)
82+
83+
// extract the element specified by the instance
84+
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
85+
var referenceObject string
86+
87+
if len(val) > 0 {
88+
referenceIndex, _ := strconv.Atoi(val[1])
89+
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
90+
found := decodedObj.([]any)[referenceIndex]
91+
recoded, _ := json.MarshalIndent(found, "", " ")
92+
referenceObject = string(recoded)
93+
}
94+
}
95+
if referenceObject == "" {
96+
referenceObject = string(responseBody)
97+
}
98+
7799
violation := &errors.SchemaValidationFailure{
78-
Reason: er.Error,
79-
Location: er.KeywordLocation,
80-
OriginalError: jk,
100+
Reason: er.Error,
101+
Location: er.KeywordLocation,
102+
ReferenceSchema: string(renderedSchema),
103+
ReferenceObject: referenceObject,
104+
OriginalError: jk,
81105
}
82106
// if we have a location within the schema, add it to the error
83107
if located != nil {

schema_validation/validate_schema.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import (
1313
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
1414
"go.uber.org/zap"
1515
"gopkg.in/yaml.v3"
16+
"reflect"
17+
"regexp"
18+
"strconv"
1619
"strings"
1720
)
1821

@@ -36,6 +39,8 @@ type SchemaValidator interface {
3639
ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*errors.ValidationError)
3740
}
3841

42+
var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
43+
3944
type schemaValidator struct {
4045
logger *zap.SugaredLogger
4146
}
@@ -110,11 +115,30 @@ func validateSchema(schema *base.Schema, payload []byte, decodedObject interface
110115

111116
// locate the violated property in the schema
112117
located := LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)
118+
119+
// extract the element specified by the instance
120+
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
121+
var referenceObject string
122+
123+
if len(val) > 0 {
124+
referenceIndex, _ := strconv.Atoi(val[1])
125+
if reflect.ValueOf(decodedObject).Type().Kind() == reflect.Slice {
126+
found := decodedObject.([]any)[referenceIndex]
127+
recoded, _ := json.MarshalIndent(found, "", " ")
128+
referenceObject = string(recoded)
129+
}
130+
}
131+
if referenceObject == "" {
132+
referenceObject = string(payload)
133+
}
134+
113135
violation := &errors.SchemaValidationFailure{
114136
Reason: er.Error,
115137
Location: er.InstanceLocation,
116138
DeepLocation: er.KeywordLocation,
117139
AbsoluteLocation: er.AbsoluteKeywordLocation,
140+
ReferenceSchema: string(renderedSchema),
141+
ReferenceObject: referenceObject,
118142
OriginalError: jk,
119143
}
120144
// if we have a location within the schema, add it to the error

0 commit comments

Comments
 (0)