Skip to content

Commit 6daac1b

Browse files
committed
Added more upgrades to schema data validation
Signed-off-by: Dave Shanley <[email protected]>
1 parent a052a86 commit 6daac1b

File tree

3 files changed

+151
-121
lines changed

3 files changed

+151
-121
lines changed

requests/validate_request.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,18 @@ func ValidateRequestSchema(
9898
}
9999
// if we have a location within the schema, add it to the error
100100
if located != nil {
101+
102+
line := located.Line
103+
// if the located node is a map or an array, then the actual human interpretable
104+
// line on which the violation occurred is the line of the key, not the value.
105+
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
106+
if line > 0 {
107+
line--
108+
}
109+
}
110+
101111
// location of the violation within the rendered schema.
102-
violation.Line = located.Line
112+
violation.Line = line
103113
violation.Column = located.Column
104114
}
105115
schemaValidationErrors = append(schemaValidationErrors, violation)

responses/validate_response.go

Lines changed: 128 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44
package responses
55

66
import (
7-
"bytes"
8-
"encoding/json"
9-
"fmt"
10-
"github.com/pb33f/libopenapi-validator/errors"
11-
"github.com/pb33f/libopenapi-validator/helpers"
12-
"github.com/pb33f/libopenapi-validator/schema_validation"
13-
"github.com/pb33f/libopenapi/datamodel/high/base"
14-
"github.com/santhosh-tekuri/jsonschema/v5"
15-
"gopkg.in/yaml.v3"
16-
"io"
17-
"net/http"
18-
"reflect"
19-
"regexp"
20-
"strconv"
21-
"strings"
7+
"bytes"
8+
"encoding/json"
9+
"fmt"
10+
"github.com/pb33f/libopenapi-validator/errors"
11+
"github.com/pb33f/libopenapi-validator/helpers"
12+
"github.com/pb33f/libopenapi-validator/schema_validation"
13+
"github.com/pb33f/libopenapi/datamodel/high/base"
14+
"github.com/santhosh-tekuri/jsonschema/v5"
15+
"gopkg.in/yaml.v3"
16+
"io"
17+
"net/http"
18+
"reflect"
19+
"regexp"
20+
"strconv"
21+
"strings"
2222
)
2323

2424
var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
@@ -29,107 +29,117 @@ var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)
2929
//
3030
// This function is used by the ValidateResponseBody function, but can be used independently.
3131
func ValidateResponseSchema(
32-
request *http.Request,
33-
response *http.Response,
34-
schema *base.Schema,
35-
renderedSchema,
36-
jsonSchema []byte) (bool, []*errors.ValidationError) {
37-
38-
var validationErrors []*errors.ValidationError
39-
40-
responseBody, _ := io.ReadAll(response.Body)
41-
42-
// close the request body, so it can be re-read later by another player in the chain
43-
_ = response.Body.Close()
44-
response.Body = io.NopCloser(bytes.NewBuffer(responseBody))
45-
46-
var decodedObj interface{}
47-
_ = json.Unmarshal(responseBody, &decodedObj)
48-
49-
// no response body? failed to decode anything? nothing to do here.
50-
if responseBody == nil || decodedObj == nil {
51-
return true, nil
52-
}
53-
54-
// create a new jsonschema compiler and add in the rendered JSON schema.
55-
compiler := jsonschema.NewCompiler()
56-
fName := fmt.Sprintf("%s.json", helpers.ResponseBodyValidation)
57-
_ = compiler.AddResource(fName,
58-
strings.NewReader(string(jsonSchema)))
59-
jsch, _ := compiler.Compile(fName)
60-
61-
// validate the object against the schema
62-
scErrs := jsch.Validate(decodedObj)
63-
if scErrs != nil {
64-
jk := scErrs.(*jsonschema.ValidationError)
65-
66-
// flatten the validationErrors
67-
schFlatErrs := jk.BasicOutput().Errors
68-
var schemaValidationErrors []*errors.SchemaValidationFailure
69-
for q := range schFlatErrs {
70-
er := schFlatErrs[q]
71-
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
72-
continue // ignore this error, it's useless tbh, utter noise.
73-
}
74-
if er.Error != "" {
75-
76-
// re-encode the schema.
77-
var renderedNode yaml.Node
78-
_ = yaml.Unmarshal(renderedSchema, &renderedNode)
79-
80-
// locate the violated property in the schema
81-
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-
99-
violation := &errors.SchemaValidationFailure{
100-
Reason: er.Error,
101-
Location: er.KeywordLocation,
102-
ReferenceSchema: string(renderedSchema),
103-
ReferenceObject: referenceObject,
104-
OriginalError: jk,
105-
}
106-
// if we have a location within the schema, add it to the error
107-
if located != nil {
108-
// location of the violation within the rendered schema.
109-
violation.Line = located.Line
110-
violation.Column = located.Column
111-
}
112-
schemaValidationErrors = append(schemaValidationErrors, violation)
113-
}
114-
}
115-
116-
// add the error to the list
117-
validationErrors = append(validationErrors, &errors.ValidationError{
118-
ValidationType: helpers.ResponseBodyValidation,
119-
ValidationSubType: helpers.Schema,
120-
Message: fmt.Sprintf("%d response body for '%s' failed to validate schema",
121-
response.StatusCode, request.URL.Path),
122-
Reason: fmt.Sprintf("The response body for status code '%d' is defined as an object. "+
123-
"However, it does not meet the schema requirements of the specification", response.StatusCode),
124-
SpecLine: schema.GoLow().Type.KeyNode.Line,
125-
SpecCol: schema.GoLow().Type.KeyNode.Column,
126-
SchemaValidationErrors: schemaValidationErrors,
127-
HowToFix: errors.HowToFixInvalidSchema,
128-
Context: string(renderedSchema), // attach the rendered schema to the error
129-
})
130-
}
131-
if len(validationErrors) > 0 {
132-
return false, validationErrors
133-
}
134-
return true, nil
32+
request *http.Request,
33+
response *http.Response,
34+
schema *base.Schema,
35+
renderedSchema,
36+
jsonSchema []byte) (bool, []*errors.ValidationError) {
37+
38+
var validationErrors []*errors.ValidationError
39+
40+
responseBody, _ := io.ReadAll(response.Body)
41+
42+
// close the request body, so it can be re-read later by another player in the chain
43+
_ = response.Body.Close()
44+
response.Body = io.NopCloser(bytes.NewBuffer(responseBody))
45+
46+
var decodedObj interface{}
47+
_ = json.Unmarshal(responseBody, &decodedObj)
48+
49+
// no response body? failed to decode anything? nothing to do here.
50+
if responseBody == nil || decodedObj == nil {
51+
return true, nil
52+
}
53+
54+
// create a new jsonschema compiler and add in the rendered JSON schema.
55+
compiler := jsonschema.NewCompiler()
56+
fName := fmt.Sprintf("%s.json", helpers.ResponseBodyValidation)
57+
_ = compiler.AddResource(fName,
58+
strings.NewReader(string(jsonSchema)))
59+
jsch, _ := compiler.Compile(fName)
60+
61+
// validate the object against the schema
62+
scErrs := jsch.Validate(decodedObj)
63+
if scErrs != nil {
64+
jk := scErrs.(*jsonschema.ValidationError)
65+
66+
// flatten the validationErrors
67+
schFlatErrs := jk.BasicOutput().Errors
68+
var schemaValidationErrors []*errors.SchemaValidationFailure
69+
for q := range schFlatErrs {
70+
er := schFlatErrs[q]
71+
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
72+
continue // ignore this error, it's useless tbh, utter noise.
73+
}
74+
if er.Error != "" {
75+
76+
// re-encode the schema.
77+
var renderedNode yaml.Node
78+
_ = yaml.Unmarshal(renderedSchema, &renderedNode)
79+
80+
// locate the violated property in the schema
81+
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+
99+
violation := &errors.SchemaValidationFailure{
100+
Reason: er.Error,
101+
Location: er.KeywordLocation,
102+
ReferenceSchema: string(renderedSchema),
103+
ReferenceObject: referenceObject,
104+
OriginalError: jk,
105+
}
106+
// if we have a location within the schema, add it to the error
107+
if located != nil {
108+
109+
line := located.Line
110+
// if the located node is a map or an array, then the actual human interpretable
111+
// line on which the violation occurred is the line of the key, not the value.
112+
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
113+
if line > 0 {
114+
line--
115+
}
116+
}
117+
118+
// location of the violation within the rendered schema.
119+
violation.Line = line
120+
violation.Column = located.Column
121+
}
122+
schemaValidationErrors = append(schemaValidationErrors, violation)
123+
}
124+
}
125+
126+
// add the error to the list
127+
validationErrors = append(validationErrors, &errors.ValidationError{
128+
ValidationType: helpers.ResponseBodyValidation,
129+
ValidationSubType: helpers.Schema,
130+
Message: fmt.Sprintf("%d response body for '%s' failed to validate schema",
131+
response.StatusCode, request.URL.Path),
132+
Reason: fmt.Sprintf("The response body for status code '%d' is defined as an object. "+
133+
"However, it does not meet the schema requirements of the specification", response.StatusCode),
134+
SpecLine: schema.GoLow().Type.KeyNode.Line,
135+
SpecCol: schema.GoLow().Type.KeyNode.Column,
136+
SchemaValidationErrors: schemaValidationErrors,
137+
HowToFix: errors.HowToFixInvalidSchema,
138+
Context: string(renderedSchema), // attach the rendered schema to the error
139+
})
140+
}
141+
if len(validationErrors) > 0 {
142+
return false, validationErrors
143+
}
144+
return true, nil
135145
}

schema_validation/validate_document.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/pb33f/libopenapi-validator/helpers"
1111
"github.com/santhosh-tekuri/jsonschema/v5"
1212
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
13+
"gopkg.in/yaml.v3"
1314
"strings"
1415
)
1516

@@ -45,7 +46,6 @@ func ValidateOpenAPIDocument(doc libopenapi.Document) (bool, []*errors.Validatio
4546
if er.Error != "" {
4647

4748
// locate the violated property in the schema
48-
4949
located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.KeywordLocation)
5050
if located == nil {
5151
// try again with the instance location
@@ -58,10 +58,20 @@ func ValidateOpenAPIDocument(doc libopenapi.Document) (bool, []*errors.Validatio
5858
AbsoluteLocation: er.AbsoluteKeywordLocation,
5959
OriginalError: jk,
6060
}
61+
6162
// if we have a location within the schema, add it to the error
6263
if located != nil {
64+
line := located.Line
65+
// if the located node is a map or an array, then the actual human interpretable
66+
// line on which the violation occurred is the line of the key, not the value.
67+
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
68+
if line > 0 {
69+
line--
70+
}
71+
}
72+
6373
// location of the violation within the rendered schema.
64-
violation.Line = located.Line
74+
violation.Line = line
6575
violation.Column = located.Column
6676
}
6777
schemaValidationErrors = append(schemaValidationErrors, violation)

0 commit comments

Comments
 (0)