Merge pull request #161 from notjames/schema-excptn-bug
Some effort to make invalid schema exceptions more clearly separate from invalid configs
diff --git a/README.md b/README.md
index 83ad31c..fe43659 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[![GoDoc](https://godoc.org/github.com/xeipuuv/gojsonschema?status.svg)](https://godoc.org/github.com/xeipuuv/gojsonschema)
[![Build Status](https://travis-ci.org/xeipuuv/gojsonschema.svg)](https://travis-ci.org/xeipuuv/gojsonschema)
# gojsonschema
@@ -224,7 +225,7 @@
Learn more about what types of template functions you can use in `ErrorTemplateFuncs` by referring to Go's [text/template FuncMap](https://golang.org/pkg/text/template/#FuncMap) type.
## Formats
-JSON Schema allows for optional "format" property to validate strings against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
+JSON Schema allows for optional "format" property to validate instances against well-known formats. gojsonschema ships with all of the formats defined in the spec that you can use like this:
````json
{"type": "string", "format": "email"}
````
@@ -237,8 +238,14 @@
type RoleFormatChecker struct {}
// Ensure it meets the gojsonschema.FormatChecker interface
-func (f RoleFormatChecker) IsFormat(input string) bool {
- return strings.HasPrefix("ROLE_", input)
+func (f RoleFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ return strings.HasPrefix("ROLE_", asString)
}
// Add it to the library
@@ -250,6 +257,37 @@
{"type": "string", "format": "role"}
````
+Another example would be to check if the provided integer matches an id on database:
+
+JSON schema:
+```json
+{"type": "integer", "format": "ValidUserId"}
+```
+
+```go
+// Define the format checker
+type ValidUserIdFormatChecker struct {}
+
+// Ensure it meets the gojsonschema.FormatChecker interface
+func (f ValidUserIdFormatChecker) IsFormat(input interface{}) bool {
+
+ asFloat64, ok := input.(float64) // Numbers are always float64 here
+ if ok == false {
+ return false
+ }
+
+ // XXX
+ // do the magic on the database looking for the int(asFloat64)
+
+ return true
+}
+
+// Add it to the library
+gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{})
+````
+
+
+
## Uses
gojsonschema uses the following test suite :
diff --git a/format_checkers.go b/format_checkers.go
index 94bd095..c6a0792 100644
--- a/format_checkers.go
+++ b/format_checkers.go
@@ -3,7 +3,6 @@
import (
"net"
"net/url"
- "reflect"
"regexp"
"strings"
"time"
@@ -12,7 +11,7 @@
type (
// FormatChecker is the interface all formatters added to FormatCheckerChain must implement
FormatChecker interface {
- IsFormat(input string) bool
+ IsFormat(input interface{}) bool
}
// FormatCheckerChain holds the formatters
@@ -125,32 +124,50 @@
return false
}
- if !isKind(input, reflect.String) {
+ return f.IsFormat(input)
+}
+
+func (f EmailFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
return false
}
- inputString := input.(string)
-
- return f.IsFormat(inputString)
-}
-
-func (f EmailFormatChecker) IsFormat(input string) bool {
- return rxEmail.MatchString(input)
+ return rxEmail.MatchString(asString)
}
// Credit: https://github.com/asaskevich/govalidator
-func (f IPV4FormatChecker) IsFormat(input string) bool {
- ip := net.ParseIP(input)
- return ip != nil && strings.Contains(input, ".")
+func (f IPV4FormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ ip := net.ParseIP(asString)
+ return ip != nil && strings.Contains(asString, ".")
}
// Credit: https://github.com/asaskevich/govalidator
-func (f IPV6FormatChecker) IsFormat(input string) bool {
- ip := net.ParseIP(input)
- return ip != nil && strings.Contains(input, ":")
+func (f IPV6FormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ ip := net.ParseIP(asString)
+ return ip != nil && strings.Contains(asString, ":")
}
-func (f DateTimeFormatChecker) IsFormat(input string) bool {
+func (f DateTimeFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
formats := []string{
"15:04:05",
"15:04:05Z07:00",
@@ -160,7 +177,7 @@
}
for _, format := range formats {
- if _, err := time.Parse(format, input); err == nil {
+ if _, err := time.Parse(format, asString); err == nil {
return true
}
}
@@ -168,8 +185,14 @@
return false
}
-func (f URIFormatChecker) IsFormat(input string) bool {
- u, err := url.Parse(input)
+func (f URIFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ u, err := url.Parse(asString)
if err != nil || u.Scheme == "" {
return false
}
@@ -177,25 +200,49 @@
return true
}
-func (f URIReferenceFormatChecker) IsFormat(input string) bool {
- _, err := url.Parse(input)
+func (f URIReferenceFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ _, err := url.Parse(asString)
return err == nil
}
-func (f HostnameFormatChecker) IsFormat(input string) bool {
- return rxHostname.MatchString(input) && len(input) < 256
+func (f HostnameFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ return rxHostname.MatchString(asString) && len(asString) < 256
}
-func (f UUIDFormatChecker) IsFormat(input string) bool {
- return rxUUID.MatchString(input)
+func (f UUIDFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ return rxUUID.MatchString(asString)
}
// IsFormat implements FormatChecker interface.
-func (f RegexFormatChecker) IsFormat(input string) bool {
- if input == "" {
+func (f RegexFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ if asString == "" {
return true
}
- _, err := regexp.Compile(input)
+ _, err := regexp.Compile(asString)
if err != nil {
return false
}
diff --git a/json_schema_test_suite/format/data_29.json b/json_schema_test_suite/format/data_29.json
new file mode 100644
index 0000000..f599e28
--- /dev/null
+++ b/json_schema_test_suite/format/data_29.json
@@ -0,0 +1 @@
+10
diff --git a/json_schema_test_suite/format/data_30.json b/json_schema_test_suite/format/data_30.json
new file mode 100644
index 0000000..b4de394
--- /dev/null
+++ b/json_schema_test_suite/format/data_30.json
@@ -0,0 +1 @@
+11
diff --git a/json_schema_test_suite/format/schema_7.json b/json_schema_test_suite/format/schema_7.json
new file mode 100644
index 0000000..4743e21
--- /dev/null
+++ b/json_schema_test_suite/format/schema_7.json
@@ -0,0 +1 @@
+{"type": "number", "format": "evenNumber"}
diff --git a/json_schema_test_suite/ref/schema_6.json b/json_schema_test_suite/ref/schema_6.json
new file mode 100644
index 0000000..a91d885
--- /dev/null
+++ b/json_schema_test_suite/ref/schema_6.json
@@ -0,0 +1 @@
+{"type":"object","additionalProperties":false,"definitions":{"x":{"type":"integer"}}}
diff --git a/schema.go b/schema.go
index cc6cdbc..f1fbde3 100644
--- a/schema.go
+++ b/schema.go
@@ -27,7 +27,6 @@
package gojsonschema
import (
- // "encoding/json"
"errors"
"reflect"
"regexp"
@@ -56,22 +55,30 @@
d.documentReference = ref
d.referencePool = newSchemaReferencePool()
+ var spd *schemaPoolDocument
var doc interface{}
if ref.String() != "" {
// Get document from schema pool
- spd, err := d.pool.GetDocument(d.documentReference)
+ spd, err = d.pool.GetDocument(d.documentReference)
if err != nil {
return nil, err
}
doc = spd.Document
+
+ // Deal with fragment pointers
+ jsonPointer := ref.GetPointer()
+ doc, _, err = jsonPointer.Get(doc)
+ if err != nil {
+ return nil, err
+ }
} else {
// Load JSON directly
doc, err = l.LoadJSON()
if err != nil {
return nil, err
}
- d.pool.SetStandaloneDocument(doc)
}
+ d.pool.SetStandaloneDocument(doc)
err = d.parse(doc)
if err != nil {
@@ -113,12 +120,48 @@
},
))
}
+ if currentSchema.parent == nil {
+ currentSchema.ref = &d.documentReference
+ currentSchema.id = &d.documentReference
+ }
+
+ if currentSchema.id == nil && currentSchema.parent != nil {
+ currentSchema.id = currentSchema.parent.id
+ }
m := documentNode.(map[string]interface{})
- if currentSchema == d.rootSchema {
- currentSchema.ref = &d.documentReference
+ // id
+ if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) {
+ return errors.New(formatErrorDescription(
+ Locale.InvalidType(),
+ ErrorDetails{
+ "expected": TYPE_STRING,
+ "given": KEY_ID,
+ },
+ ))
}
+ if k, ok := m[KEY_ID].(string); ok {
+ jsonReference, err := gojsonreference.NewJsonReference(k)
+ if err != nil {
+ return err
+ }
+ if currentSchema == d.rootSchema {
+ currentSchema.id = &jsonReference
+ } else {
+ ref, err := currentSchema.parent.id.Inherits(jsonReference)
+ if err != nil {
+ return err
+ }
+ currentSchema.id = ref
+ }
+ }
+
+ // Add schema to document cache. The same id is passed down to subsequent
+ // subschemas, but as only the first and top one is used it will always reference
+ // the correct schema. Doing it once here prevents having
+ // to do this same step at every corner case.
+ d.referencePool.Add(currentSchema.id.String(), currentSchema)
// $subSchema
if existsMapKey(m, KEY_SCHEMA) {
@@ -159,19 +202,17 @@
if jsonReference.HasFullUrl {
currentSchema.ref = &jsonReference
} else {
- inheritedReference, err := currentSchema.ref.Inherits(jsonReference)
+ inheritedReference, err := currentSchema.id.Inherits(jsonReference)
if err != nil {
return err
}
-
currentSchema.ref = inheritedReference
}
-
- if sch, ok := d.referencePool.Get(currentSchema.ref.String() + k); ok {
+ if sch, ok := d.referencePool.Get(currentSchema.ref.String()); ok {
currentSchema.refSchema = sch
-
} else {
- err := d.parseReference(documentNode, currentSchema, k)
+ err := d.parseReference(documentNode, currentSchema)
+
if err != nil {
return err
}
@@ -186,11 +227,23 @@
currentSchema.definitions = make(map[string]*subSchema)
for dk, dv := range m[KEY_DEFINITIONS].(map[string]interface{}) {
if isKind(dv, reflect.Map) {
- newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, ref: currentSchema.ref}
- currentSchema.definitions[dk] = newSchema
- err := d.parseSchema(dv, newSchema)
+
+ ref, err := gojsonreference.NewJsonReference("#/" + KEY_DEFINITIONS + "/" + dk)
if err != nil {
- return errors.New(err.Error())
+ return err
+ }
+
+ newSchemaID, err := currentSchema.id.Inherits(ref)
+ if err != nil {
+ return err
+ }
+ newSchema := &subSchema{property: KEY_DEFINITIONS, parent: currentSchema, id: newSchemaID}
+ currentSchema.definitions[dk] = newSchema
+
+ err = d.parseSchema(dv, newSchema)
+
+ if err != nil {
+ return err
}
} else {
return errors.New(formatErrorDescription(
@@ -214,20 +267,6 @@
}
- // id
- if existsMapKey(m, KEY_ID) && !isKind(m[KEY_ID], reflect.String) {
- return errors.New(formatErrorDescription(
- Locale.InvalidType(),
- ErrorDetails{
- "expected": TYPE_STRING,
- "given": KEY_ID,
- },
- ))
- }
- if k, ok := m[KEY_ID].(string); ok {
- currentSchema.id = &k
- }
-
// title
if existsMapKey(m, KEY_TITLE) && !isKind(m[KEY_TITLE], reflect.String) {
return errors.New(formatErrorDescription(
@@ -586,11 +625,6 @@
formatString, ok := m[KEY_FORMAT].(string)
if ok && FormatCheckers.Has(formatString) {
currentSchema.format = formatString
- } else {
- return errors.New(formatErrorDescription(
- Locale.MustBeValidFormat(),
- ErrorDetails{"key": KEY_FORMAT, "given": m[KEY_FORMAT]},
- ))
}
}
@@ -803,26 +837,32 @@
return nil
}
-func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema, reference string) error {
- var refdDocumentNode interface{}
+func (d *Schema) parseReference(documentNode interface{}, currentSchema *subSchema) error {
+ var (
+ refdDocumentNode interface{}
+ dsp *schemaPoolDocument
+ err error
+ )
jsonPointer := currentSchema.ref.GetPointer()
standaloneDocument := d.pool.GetStandaloneDocument()
- if standaloneDocument != nil {
+ newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
- var err error
+ if currentSchema.ref.HasFragmentOnly {
refdDocumentNode, _, err = jsonPointer.Get(standaloneDocument)
if err != nil {
return err
}
} else {
- dsp, err := d.pool.GetDocument(*currentSchema.ref)
+ dsp, err = d.pool.GetDocument(*currentSchema.ref)
if err != nil {
return err
}
+ newSchema.id = currentSchema.ref
refdDocumentNode, _, err = jsonPointer.Get(dsp.Document)
+
if err != nil {
return err
}
@@ -838,10 +878,8 @@
// returns the loaded referenced subSchema for the caller to update its current subSchema
newSchemaDocument := refdDocumentNode.(map[string]interface{})
- newSchema := &subSchema{property: KEY_REF, parent: currentSchema, ref: currentSchema.ref}
- d.referencePool.Add(currentSchema.ref.String()+reference, newSchema)
- err := d.parseSchema(newSchemaDocument, newSchema)
+ err = d.parseSchema(newSchemaDocument, newSchema)
if err != nil {
return err
}
diff --git a/schemaPool.go b/schemaPool.go
index f2ad641..ff9715f 100644
--- a/schemaPool.go
+++ b/schemaPool.go
@@ -62,12 +62,16 @@
func (p *schemaPool) GetDocument(reference gojsonreference.JsonReference) (*schemaPoolDocument, error) {
+ var (
+ spd *schemaPoolDocument
+ ok bool
+ err error
+ )
+
if internalLogEnabled {
internalLog("Get Document ( %s )", reference.String())
}
- var err error
-
// It is not possible to load anything that is not canonical...
if !reference.IsCanonical() {
return nil, errors.New(formatErrorDescription(
@@ -75,20 +79,10 @@
ErrorDetails{"reference": reference},
))
}
-
refToUrl := reference
refToUrl.GetUrl().Fragment = ""
- var spd *schemaPoolDocument
-
- // Try to find the requested document in the pool
- for k := range p.schemaPoolDocuments {
- if k == refToUrl.String() {
- spd = p.schemaPoolDocuments[k]
- }
- }
-
- if spd != nil {
+ if spd, ok = p.schemaPoolDocuments[refToUrl.String()]; ok {
if internalLogEnabled {
internalLog(" From pool")
}
diff --git a/schemaReferencePool.go b/schemaReferencePool.go
index 294e36a..6e5e1b5 100644
--- a/schemaReferencePool.go
+++ b/schemaReferencePool.go
@@ -62,6 +62,7 @@
if internalLogEnabled {
internalLog(fmt.Sprintf("Add Schema Reference %s to pool", ref))
}
-
- p.documents[ref] = sch
+ if _, ok := p.documents[ref]; !ok {
+ p.documents[ref] = sch
+ }
}
diff --git a/schema_test.go b/schema_test.go
index b80c30a..9c40e71 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -50,13 +50,30 @@
// Used for remote schema in ref/schema_5.json that defines "uri" and "regex" types
type alwaysTrueFormatChecker struct{}
type invoiceFormatChecker struct{}
+type evenNumberFormatChecker struct{}
-func (a alwaysTrueFormatChecker) IsFormat(input string) bool {
+func (a alwaysTrueFormatChecker) IsFormat(input interface{}) bool {
return true
}
-func (a invoiceFormatChecker) IsFormat(input string) bool {
- return rxInvoice.MatchString(input)
+func (a evenNumberFormatChecker) IsFormat(input interface{}) bool {
+
+ asFloat64, ok := input.(float64)
+ if ok == false {
+ return false
+ }
+
+ return int(asFloat64)%2 == 0
+}
+
+func (a invoiceFormatChecker) IsFormat(input interface{}) bool {
+
+ asString, ok := input.(string)
+ if ok == false {
+ return false
+ }
+
+ return rxInvoice.MatchString(asString)
}
func TestJsonSchemaTestSuite(t *testing.T) {
@@ -237,6 +254,8 @@
{"phase": "root pointer ref", "test": "recursive match", "schema": "ref/schema_0.json", "data": "ref/data_01.json", "valid": "true"},
{"phase": "root pointer ref", "test": "mismatch", "schema": "ref/schema_0.json", "data": "ref/data_02.json", "valid": "false", "errors": "additional_property_not_allowed"},
{"phase": "root pointer ref", "test": "recursive mismatch", "schema": "ref/schema_0.json", "data": "ref/data_03.json", "valid": "false", "errors": "additional_property_not_allowed"},
+ {"phase": "loader pointer ref", "test": "root ref valid", "schema": "ref/schema_6.json#/definitions/x", "data": "ref/data_40.json", "valid": "true"},
+ {"phase": "loader pointer ref", "test": "root ref invalid", "schema": "ref/schema_6.json#/definitions/x", "data": "ref/data_41.json", "valid": "false", "errors": "invalid_type"},
{"phase": "relative pointer ref to object", "test": "match", "schema": "ref/schema_1.json", "data": "ref/data_10.json", "valid": "true"},
{"phase": "relative pointer ref to object", "test": "mismatch", "schema": "ref/schema_1.json", "data": "ref/data_11.json", "valid": "false", "errors": "invalid_type"},
{"phase": "relative pointer ref to array", "test": "match array", "schema": "ref/schema_2.json", "data": "ref/data_20.json", "valid": "true"},
@@ -341,12 +360,12 @@
{"phase": "format validation", "test": "uri format is valid", "schema": "format/schema_6.json", "data": "format/data_27.json", "valid": "true"},
{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_28.json", "valid": "false", "errors": "format"},
{"phase": "format validation", "test": "uri format is invalid", "schema": "format/schema_6.json", "data": "format/data_13.json", "valid": "false", "errors": "format"},
+ {"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_29.json", "valid": "true"},
+ {"phase": "format validation", "test": "number format is valid", "schema": "format/schema_7.json", "data": "format/data_30.json", "valid": "false", "errors": "format"},
+ {"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"},
+ {"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false", "errors": "invalid_type"},
}
- //TODO Pass failed tests : id(s) as scope for references is not implemented yet
- //map[string]string{"phase": "change resolution scope", "test": "changed scope ref valid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_30.json", "valid": "true"},
- //map[string]string{"phase": "change resolution scope", "test": "changed scope ref invalid", "schema": "refRemote/schema_3.json", "data": "refRemote/data_31.json", "valid": "false"}}
-
// Setup a small http server on localhost:1234 for testing purposes
wd, err := os.Getwd()
@@ -369,6 +388,9 @@
// Custom Formatter
FormatCheckers.Add("invoice", invoiceFormatChecker{})
+ // Number Formatter
+ FormatCheckers.Add("evenNumber", evenNumberFormatChecker{})
+
// Launch tests
for testJsonIndex, testJson := range JsonSchemaTestSuiteMap {
@@ -394,6 +416,9 @@
expectedValid, _ := strconv.ParseBool(testJson["valid"])
if givenValid != expectedValid {
t.Errorf("Test failed : %s :: %s, expects %t, given %t\n", testJson["phase"], testJson["test"], expectedValid, givenValid)
+ for _, e := range result.Errors() {
+ fmt.Println("Error: " + e.Type())
+ }
}
if !givenValid && testJson["errors"] != "" {
diff --git a/subSchema.go b/subSchema.go
index 9ddbb5f..9961d92 100644
--- a/subSchema.go
+++ b/subSchema.go
@@ -36,7 +36,7 @@
const (
KEY_SCHEMA = "$subSchema"
- KEY_ID = "$id"
+ KEY_ID = "id"
KEY_REF = "$ref"
KEY_TITLE = "title"
KEY_DESCRIPTION = "description"
@@ -73,7 +73,7 @@
type subSchema struct {
// basic subSchema meta properties
- id *string
+ id *gojsonreference.JsonReference
title *string
description *string
diff --git a/validation.go b/validation.go
index 6140bd8..9afea25 100644
--- a/validation.go
+++ b/validation.go
@@ -828,5 +828,17 @@
}
}
+ // format
+ if currentSubSchema.format != "" {
+ if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) {
+ result.addError(
+ new(DoesNotMatchFormatError),
+ context,
+ value,
+ ErrorDetails{"format": currentSubSchema.format},
+ )
+ }
+ }
+
result.incrementScore()
}