Skip to content

Commit cd34d32

Browse files
committed
Add full schema pointer matching capability to schema.ValidationErrorMatch()
When a schema validation error is related to one of the logic inversion keywords ("not", "oneOf"), the schema pointer path provided in the schema validation package's validation results data does not extend past that keyword. Capability has now been added for the error to be matched against the full JSON pointers under the problematic keywords. In combination with structuring the schemas in a manner that puts the logic inversion keywords at the lowest possible level, this makes it possible to fully determine the cause of a validation failure.
1 parent 9d29588 commit cd34d32

File tree

3 files changed

+70
-13
lines changed

3 files changed

+70
-13
lines changed

check/checkdata/schema/parsevalidationresult.go

+59-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package schema
1717

1818
import (
1919
"encoding/json"
20+
"fmt"
2021
"regexp"
2122

2223
"github.com/arduino/go-paths-helper"
@@ -76,10 +77,10 @@ func validationErrorMatch(
7677
logrus.Tracef("Checking instance pointer: %s match with regexp: %s", validationError.InstancePtr, instancePointerRegexp)
7778
if instancePointerRegexp.MatchString(validationError.InstancePtr) {
7879
logrus.Tracef("Matched!")
79-
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", validationError.SchemaPtr, schemaPointerRegexp)
80-
if schemaPointerRegexp.MatchString(validationError.SchemaPtr) {
80+
matchedSchemaPointer := validationErrorSchemaPointerMatch(schemaPointerRegexp, validationError, schemasPath)
81+
if matchedSchemaPointer != "" {
8182
logrus.Tracef("Matched!")
82-
if validationErrorSchemaPointerValueMatch(schemaPointerValueRegexp, validationError, schemasPath) {
83+
if validationErrorSchemaPointerValueMatch(schemaPointerValueRegexp, validationError.SchemaURL, matchedSchemaPointer, schemasPath) {
8384
logrus.Tracef("Matched!")
8485
logrus.Tracef("Checking failure context: %v match with regexp: %s", validationError.Context, failureContextRegexp)
8586
if validationErrorContextMatch(failureContextRegexp, validationError) {
@@ -107,14 +108,67 @@ func validationErrorMatch(
107108
return false
108109
}
109110

111+
// validationErrorSchemaPointerMatch matches the JSON schema pointer related to the validation failure against a regular expression.
112+
func validationErrorSchemaPointerMatch(
113+
schemaPointerRegexp *regexp.Regexp,
114+
validationError *jsonschema.ValidationError,
115+
schemasPath *paths.Path,
116+
) string {
117+
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", validationError.SchemaPtr, schemaPointerRegexp)
118+
if schemaPointerRegexp.MatchString(validationError.SchemaPtr) {
119+
return validationError.SchemaPtr
120+
}
121+
122+
// The schema validator does not provide full pointer past logic inversion keywords to the lowest level keywords related to the validation error cause.
123+
// Therefore the sub-keywords must be checked for matches in order to be able to interpret the exact cause of the failure.
124+
if regexp.MustCompile("(/not)|(/oneOf)$").MatchString(validationError.SchemaPtr) {
125+
return validationErrorSchemaSubPointerMatch(schemaPointerRegexp, validationError.SchemaPtr, validationErrorSchemaPointerValue(validationError, schemasPath))
126+
}
127+
128+
return ""
129+
}
130+
131+
// validationErrorSchemaSubPointerMatch recursively checks JSON pointers of all keywords under the parent pointer for match against a regular expression.
132+
// The matching JSON pointer is returned.
133+
func validationErrorSchemaSubPointerMatch(schemaPointerRegexp *regexp.Regexp, parentPointer string, pointerValueObject interface{}) string {
134+
// Recurse through iterable objects.
135+
switch assertedObject := pointerValueObject.(type) {
136+
case []interface{}:
137+
for index, element := range assertedObject {
138+
// Append index to JSON pointer and check for match.
139+
matchingPointer := validationErrorSchemaSubPointerMatch(schemaPointerRegexp, fmt.Sprintf("%s/%d", parentPointer, index), element)
140+
if matchingPointer != "" {
141+
return matchingPointer
142+
}
143+
}
144+
case map[string]interface{}:
145+
for key := range assertedObject {
146+
// Append key to JSON pointer and check for match.
147+
matchingPointer := validationErrorSchemaSubPointerMatch(schemaPointerRegexp, parentPointer+"/"+key, assertedObject[key])
148+
if matchingPointer != "" {
149+
return matchingPointer
150+
}
151+
// TODO: Follow references. For now, the schema code must be written so that the problematic keywords are after the reference.
152+
}
153+
}
154+
155+
// pointerValueObject is not further iterable. Check for match against the parent JSON pointer.
156+
logrus.Tracef("Checking schema pointer: %s match with regexp: %s", parentPointer, schemaPointerRegexp)
157+
if schemaPointerRegexp.MatchString(parentPointer) {
158+
return parentPointer
159+
}
160+
return ""
161+
}
162+
110163
// validationErrorSchemaPointerValueMatch marshalls the data in the schema at the given JSON pointer and returns whether
111164
// it matches against the given regular expression.
112165
func validationErrorSchemaPointerValueMatch(
113166
schemaPointerValueRegexp *regexp.Regexp,
114-
validationError *jsonschema.ValidationError,
167+
schemaURL,
168+
schemaPointer string,
115169
schemasPath *paths.Path,
116170
) bool {
117-
marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(validationError, schemasPath))
171+
marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(schemaURL, schemaPointer, schemasPath))
118172
logrus.Tracef("Checking schema pointer value: %s match with regexp: %s", marshalledSchemaPointerValue, schemaPointerValueRegexp)
119173
if err != nil {
120174
panic(err)

check/checkdata/schema/schema.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func logValidationError(validationError *jsonschema.ValidationError, schemasPath
155155
logrus.Tracef("Instance pointer: %v", validationError.InstancePtr)
156156
logrus.Tracef("Schema URL: %s", validationError.SchemaURL)
157157
logrus.Tracef("Schema pointer: %s", validationError.SchemaPtr)
158-
logrus.Tracef("Schema pointer value: %v", schemaPointerValue(validationError, schemasPath))
158+
logrus.Tracef("Schema pointer value: %v", validationErrorSchemaPointerValue(validationError, schemasPath))
159159
logrus.Tracef("Failure context: %v", validationError.Context)
160160
logrus.Tracef("Failure context type: %T", validationError.Context)
161161

@@ -165,10 +165,15 @@ func logValidationError(validationError *jsonschema.ValidationError, schemasPath
165165
}
166166
}
167167

168+
// validationErrorSchemaPointerValue returns the object identified by the validation error's schema JSON pointer.
169+
func validationErrorSchemaPointerValue(validationError *jsonschema.ValidationError, schemasPath *paths.Path) interface{} {
170+
return schemaPointerValue(validationError.SchemaURL, validationError.SchemaPtr, schemasPath)
171+
}
172+
168173
// schemaPointerValue returns the object identified by the given JSON pointer from the schema file.
169-
func schemaPointerValue(validationError *jsonschema.ValidationError, schemasPath *paths.Path) interface{} {
170-
schemaPath := schemasPath.Join(path.Base(validationError.SchemaURL))
171-
return jsonPointerValue(validationError.SchemaPtr, schemaPath)
174+
func schemaPointerValue(schemaURL, schemaPointer string, schemasPath *paths.Path) interface{} {
175+
schemaPath := schemasPath.Join(path.Base(schemaURL))
176+
return jsonPointerValue(schemaPointer, schemaPath)
172177
}
173178

174179
// jsonPointerValue returns the object identified by the given JSON pointer from the JSON file.

check/checkdata/schema/schema_test.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package schema
22

33
import (
4-
"fmt"
54
"os"
65
"regexp"
76
"runtime"
@@ -155,14 +154,13 @@ func Test_pathURI(t *testing.T) {
155154
}
156155
}
157156

158-
func Test_schemaPointerValue(t *testing.T) {
157+
func Test_validationErrorSchemaPointerValue(t *testing.T) {
159158
validationError := jsonschema.ValidationError{
160159
SchemaURL: "https://raw.githubusercontent.com/arduino/arduino-check/main/check/checkdata/schema/testdata/referenced-schema-1.json",
161160
SchemaPtr: "#/definitions/patternObject/pattern",
162161
}
163162

164-
schemaPointerValueInterface := schemaPointerValue(&validationError, schemasPath)
165-
fmt.Printf("%T", schemaPointerValueInterface)
163+
schemaPointerValueInterface := validationErrorSchemaPointerValue(&validationError, schemasPath)
166164
schemaPointerValue, ok := schemaPointerValueInterface.(string)
167165
require.True(t, ok)
168166
require.Equal(t, "^[a-z]+$", schemaPointerValue)

0 commit comments

Comments
 (0)