Skip to content

Commit 7c8c7c7

Browse files
committed
Always check a schema has a type when returning errors.
As reported in #16, types may not always exist (so the schema is completely invalid anyway). However, it’s important to make sure that we check for the type, before using it. Signed-off-by: Dave Shanley <[email protected]>
1 parent dca35f7 commit 7c8c7c7

File tree

3 files changed

+153
-140
lines changed

3 files changed

+153
-140
lines changed

requests/validate_request.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ func ValidateRequestSchema(
116116
}
117117
}
118118

119+
line := 1
120+
col := 0
121+
if schema.GoLow().Type.KeyNode != nil {
122+
line = schema.GoLow().Type.KeyNode.Line
123+
col = schema.GoLow().Type.KeyNode.Column
124+
}
125+
119126
// add the error to the list
120127
validationErrors = append(validationErrors, &errors.ValidationError{
121128
ValidationType: helpers.RequestBodyValidation,
@@ -124,8 +131,8 @@ func ValidateRequestSchema(
124131
request.Method, request.URL.Path),
125132
Reason: "The request body is defined as an object. " +
126133
"However, it does not meet the schema requirements of the specification",
127-
SpecLine: schema.GoLow().Type.KeyNode.Line,
128-
SpecCol: schema.GoLow().Type.KeyNode.Column,
134+
SpecLine: line,
135+
SpecCol: col,
129136
SchemaValidationErrors: schemaValidationErrors,
130137
HowToFix: errors.HowToFixInvalidSchema,
131138
Context: string(renderedSchema), // attach the rendered schema to the error

responses/validate_response.go

Lines changed: 135 additions & 136 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,125 +29,124 @@ 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-
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-
line := 0
127-
col := 0
128-
if schema.GoLow().Type.KeyNode != nil {
129-
line = schema.GoLow().Type.KeyNode.Line
130-
col = schema.GoLow().Type.KeyNode.Column
131-
}
132-
133-
134-
// add the error to the list
135-
validationErrors = append(validationErrors, &errors.ValidationError{
136-
ValidationType: helpers.ResponseBodyValidation,
137-
ValidationSubType: helpers.Schema,
138-
Message: fmt.Sprintf("%d response body for '%s' failed to validate schema",
139-
response.StatusCode, request.URL.Path),
140-
Reason: fmt.Sprintf("The response body for status code '%d' is defined as an object. "+
141-
"However, it does not meet the schema requirements of the specification", response.StatusCode),
142-
SpecLine: line,
143-
SpecCol: col,
144-
SchemaValidationErrors: schemaValidationErrors,
145-
HowToFix: errors.HowToFixInvalidSchema,
146-
Context: string(renderedSchema), // attach the rendered schema to the error
147-
})
148-
}
149-
if len(validationErrors) > 0 {
150-
return false, validationErrors
151-
}
152-
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+
line := 1
127+
col := 0
128+
if schema.GoLow().Type.KeyNode != nil {
129+
line = schema.GoLow().Type.KeyNode.Line
130+
col = schema.GoLow().Type.KeyNode.Column
131+
}
132+
133+
// add the error to the list
134+
validationErrors = append(validationErrors, &errors.ValidationError{
135+
ValidationType: helpers.ResponseBodyValidation,
136+
ValidationSubType: helpers.Schema,
137+
Message: fmt.Sprintf("%d response body for '%s' failed to validate schema",
138+
response.StatusCode, request.URL.Path),
139+
Reason: fmt.Sprintf("The response body for status code '%d' is defined as an object. "+
140+
"However, it does not meet the schema requirements of the specification", response.StatusCode),
141+
SpecLine: line,
142+
SpecCol: col,
143+
SchemaValidationErrors: schemaValidationErrors,
144+
HowToFix: errors.HowToFixInvalidSchema,
145+
Context: string(renderedSchema), // attach the rendered schema to the error
146+
})
147+
}
148+
if len(validationErrors) > 0 {
149+
return false, validationErrors
150+
}
151+
return true, nil
153152
}

schema_validation/validate_schema.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,21 @@ func validateSchema(schema *base.Schema, payload []byte, decodedObject interface
161161
}
162162
}
163163
}
164+
line := 1
165+
col := 0
166+
if schema.GoLow().Type.KeyNode != nil {
167+
line = schema.GoLow().Type.KeyNode.Line
168+
col = schema.GoLow().Type.KeyNode.Column
169+
}
170+
164171

165172
// add the error to the list
166173
validationErrors = append(validationErrors, &errors.ValidationError{
167174
ValidationType: helpers.Schema,
168175
Message: "schema does not pass validation",
169176
Reason: "Schema failed to validate against the contract requirements",
170-
SpecLine: schema.GoLow().Type.KeyNode.Line,
171-
SpecCol: schema.GoLow().Type.KeyNode.Column,
177+
SpecLine: line,
178+
SpecCol: col,
172179
SchemaValidationErrors: schemaValidationErrors,
173180
HowToFix: errors.HowToFixInvalidSchema,
174181
Context: string(renderedSchema), // attach the rendered schema to the error

0 commit comments

Comments
 (0)