Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ValidationRequiredError error context #1

Merged
merged 1 commit into from
Nov 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type ValidationError struct {
// that failed to satisfy
SchemaPtr string

// Context represents error context for this specific validation error.
Context ValidationContext

// Causes details the nested validation errors
Causes []*ValidationError
}
Expand Down Expand Up @@ -78,7 +81,7 @@ func (ve *ValidationError) Error() string {
}

func validationError(schemaPtr string, format string, a ...interface{}) *ValidationError {
return &ValidationError{fmt.Sprintf(format, a...), "", "", schemaPtr, nil}
return &ValidationError{fmt.Sprintf(format, a...), "", "", schemaPtr, nil, nil}
}

func addContext(instancePtr, schemaPtr string, err error) error {
Expand All @@ -87,6 +90,9 @@ func addContext(instancePtr, schemaPtr string, err error) error {
if len(ve.SchemaURL) == 0 {
ve.SchemaPtr = joinPtr(schemaPtr, ve.SchemaPtr)
}
if ve.Context != nil {
ve.Context.AddContext(instancePtr, ve.SchemaPtr)
}
for _, cause := range ve.Causes {
addContext(instancePtr, schemaPtr, cause)
}
Expand All @@ -111,6 +117,9 @@ func finishInstanceContext(err error) {
} else {
ve.InstancePtr = "#/" + ve.InstancePtr
}
if ve.Context != nil {
ve.Context.FinishInstanceContext()
}
for _, cause := range ve.Causes {
finishInstanceContext(cause)
}
Expand Down
4 changes: 2 additions & 2 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,11 @@ func (s *Schema) validate(v interface{}) error {
var missing []string
for _, pname := range s.Required {
if _, ok := v[pname]; !ok {
missing = append(missing, strconv.Quote(pname))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pname may contain comma or space. because of this it was quoted in message

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the raw JSON pointers here (e.g. #/foo/bar) because we want to pass that to the error details. The error message itself is still being quoted: https://github.com/ory/jsonschema/pull/1/files#diff-02934e0fdc85f35aa62e19cf7c6f6bcaR39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. but before including pname in json pointer, some special characters needs to be escaped.
check https://github.com/santhosh-tekuri/jsonschema/blob/master/schema.go#L557

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! I though joinPtr would do that but I assumed that incorrectly.

missing = append(missing, pname)
}
}
if len(missing) > 0 {
errors = append(errors, validationError("required", "missing properties: %s", strings.Join(missing, ", ")))
errors = append(errors, validationRequiredError(missing))
}
}

Expand Down
54 changes: 54 additions & 0 deletions testdata/errors/required.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"bar": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
},
"required": [
"bar"
]
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"object": {
"type": "object",
"properties": {
"object": {
"type": "object",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "string"
}
},
"required": [
"foo",
"bar"
]
}
},
"required": [
"object"
]
}
},
"required": [
"object"
]
}
]
50 changes: 50 additions & 0 deletions validation_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package jsonschema

import (
"fmt"
"strconv"
"strings"
)

// ValidationContext
type ValidationContext interface {
AddContext(instancePtr, schemaPtr string)
FinishInstanceContext()
}

// ValidationContextRequired is used as error context when one or more required properties are missing.
type ValidationContextRequired struct {
// Missing contains JSON Pointers to all missing properties.
Missing []string
}

func (r *ValidationContextRequired) AddContext(instancePtr, _ string) {
for k, p := range r.Missing {
r.Missing[k] = joinPtr(instancePtr, p)
}
}

func (r *ValidationContextRequired) FinishInstanceContext() {
for k, p := range r.Missing {
if len(p) == 0 {
r.Missing[k] = "#"
} else {
r.Missing[k] = "#/" + p
}
}
}

func validationRequiredError(properties []string) *ValidationError {
missing := make([]string, len(properties))

for k := range missing {
missing[k] = strconv.Quote(properties[k])
properties[k] = escape(properties[k])
}

return &ValidationError{
SchemaPtr: "required",
Message: fmt.Sprintf("missing properties: %s", strings.Join(missing, ", ")),
Context: &ValidationContextRequired{Missing: properties},
}
}
86 changes: 86 additions & 0 deletions validation_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package jsonschema_test

import (
"bytes"
"fmt"
"reflect"
"testing"

"github.com/santhosh-tekuri/jsonschema/v2"
)

func TestErrorsContext(t *testing.T) {
for k, tc := range []struct {
path string
doc string
expected interface{}
}{
{
path: "testdata/errors/required.json#/0",
doc: `{}`,
expected: &jsonschema.ValidationContextRequired{Missing: []string{"#/bar"}},
},
{
path: "testdata/errors/required.json#/0",
doc: `{"bar":{}}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/bar/foo"},
},
},
{
path: "testdata/errors/required.json#/1",
doc: `{"object":{"object":{"foo":"foo"}}}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/object/object/bar"},
},
},
{
path: "testdata/errors/required.json#/1",
doc: `{"object":{"object":{"bar":"bar"}}}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/object/object/foo"},
},
},
{
path: "testdata/errors/required.json#/1",
doc: `{"object":{"object":{}}}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/object/object/foo", "#/object/object/bar"},
},
},
{
path: "testdata/errors/required.json#/1",
doc: `{"object":{}}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/object/object"},
},
},
{
path: "testdata/errors/required.json#/1",
doc: `{}`,
expected: &jsonschema.ValidationContextRequired{
Missing: []string{"#/object"},
},
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
var (
schema = jsonschema.MustCompile(tc.path)
err = schema.Validate(bytes.NewBufferString(tc.doc))
)

if err == nil {
t.Errorf("Expected error but got nil")
return
}

var (
actual = err.(*jsonschema.ValidationError).Context
)

if !reflect.DeepEqual(tc.expected, actual) {
t.Errorf("expected:\t%#v\n\tactual:\t%#v", tc.expected, actual)
}
})
}
}