Skip to content

Commit

Permalink
refactor(jsonschema): reworking json schema (migration to draft2019_09)(
Browse files Browse the repository at this point in the history
#65)

* refactor(jsonschema): reworking how we handle json schema parsing and validating

* refactor(jsonschema): refactored jsonschema and implmented draft2019_09

* refactor(jsonschema): added more tests, made schemas autoload draft2019 keywords

* refactor(jsonschema): cleanup, updated README

* refactor(jsonschema): fixing bugs, implementing more keywords, cleanup

* refactor(jsonschema): further cleanup

* refactor(jsonschema): improving the validation API
  • Loading branch information
Arqu committed May 21, 2020
1 parent 7774a18 commit bb2a1cf
Show file tree
Hide file tree
Showing 36 changed files with 4,447 additions and 2,514 deletions.
3 changes: 2 additions & 1 deletion .gitignore
@@ -1,2 +1,3 @@
.DS_Store
coverage.txt
coverage.txt
.vscode
138 changes: 81 additions & 57 deletions README.md
Expand Up @@ -13,7 +13,7 @@ golang implementation of the [JSON Schema Specification](http://json-schema.org/
* Encode schemas back to JSON
* Supply Your own Custom Validators
* Uses Standard Go idioms
* Fastest Go implementation of [JSON Schema validators](http://json-schema.org/implementations.html#validators) (draft 7 only, benchmarks are [here](https://github.com/TheWildBlue/validator-benchmarks) - thanks [@TheWildBlue](https://github.com/TheWildBlue)!)
* Fastest Go implementation of [JSON Schema validators](http://json-schema.org/implementations.html#validators) (draft2019_9 only, (old - draft 7) benchmarks are [here](https://github.com/TheWildBlue/validator-benchmarks) - thanks [@TheWildBlue](https://github.com/TheWildBlue)!)

### Getting Involved

Expand Down Expand Up @@ -41,69 +41,85 @@ import (

func main() {
var schemaData = []byte(`{
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
},
"friends": {
"type" : "array",
"items" : { "title" : "REFERENCE", "$ref" : "#" }
}
},
"required": ["firstName", "lastName"]
}`)

rs := &jsonschema.RootSchema{}
if err := json.Unmarshal(schemaData, rs); err != nil {
panic("unmarshal schema: " + err.Error())
}
"$id": "https://qri.io/schema/",
"$comment" : "sample comment",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
},
"friends": {
"type" : "array",
"items" : { "title" : "REFERENCE", "$ref" : "#" }
}
},
"required": ["firstName", "lastName"]
}`)

rs := &Schema{}
if err := json.Unmarshal(schemaData, rs); err != nil {
panic("unmarshal schema: " + err.Error())
}

var valid = []byte(`{
var valid = []byte(`{
"firstName" : "George",
"lastName" : "Michael"
}`)
errs, err := rs.ValidateBytes(valid)
if err != nil {
panic(err)
}

if errors, _ := rs.ValidateBytes(valid); len(errors) > 0 {
panic(errors)
}
if len(errs) > 0 {
fmt.Println(errs[0].Error())
}

var invalidPerson = []byte(`{
var invalidPerson = []byte(`{
"firstName" : "Prince"
}`)
if errors, _ := rs.ValidateBytes(invalidPerson); len(errors) > 0 {
fmt.Println(errors[0].Error())

errs, err = rs.ValidateBytes(invalidPerson)
if err != nil {
panic(err)
}
if len(errs) > 0 {
fmt.Println(errs[0].Error())
}

var invalidFriend = []byte(`{
var invalidFriend = []byte(`{
"firstName" : "Jay",
"lastName" : "Z",
"friends" : [{
"firstName" : "Nas"
}]
}`)
if errors, _ := rs.ValidateBytes(invalidFriend); len(errors) > 0 {
fmt.Println(errors[0].Error())
errs, err = rs.ValidateBytes(invalidFriend)
if err != nil {
panic(err)
}
if len(errs) > 0 {
fmt.Println(errs[0].Error())
}
}
```

## Custom Validators
## Custom Keywords

The [godoc](https://godoc.org/github.com/qri-io/jsonschema) gives an example of how to supply your own validators to extend the standard keywords supported by the spec.

It involves two steps that should happen _before_ allocating any RootSchema instances that use the validator:
1. create a custom type that implements the `Validator` interface
2. call RegisterValidator with the keyword you'd like to detect in JSON, and a `ValMaker` function.
It involves three steps that should happen _before_ allocating any Schema instances that use the validator:
1. create a custom type that implements the `Keyword` interface
2. Load the appropriate draft keyword set (see `draft2019_09_keywords.go`)
3. call RegisterKeyword with the keyword you'd like to detect in JSON, and a `KeyMaker` function.


```go
Expand All @@ -112,50 +128,58 @@ package main
import (
"encoding/json"
"fmt"

"github.com/qri-io/jsonschema"
)

// your custom validator
type IsFoo bool

// newIsFoo is a jsonschama.ValMaker
func newIsFoo() jsonschema.Validator {
// newIsFoo is a jsonschama.KeyMaker
func newIsFoo() Keyword {
return new(IsFoo)
}

// Validate implements jsonschema.Validator
func (f IsFoo) Validate(data interface{}) []jsonschema.ValError {
// Validate implements jsonschema.Keyword
func (f *IsFoo) Validate(propPath string, data interface{}, errs *[]KeyError) {}

// Register implements jsonschema.Keyword
func (f *IsFoo) Register(uri string, registry *SchemaRegistry) {}

// Resolve implements jsonschema.Keyword
func (f *IsFoo) Resolve(pointer jptr.Pointer, uri string) *Schema {
return nil
}

// ValidateKeyword implements jsonschema.Keyword
func (f *IsFoo) ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{}) {
if str, ok := data.(string); ok {
if str != "foo" {
return []jsonschema.ValError{
{Message: fmt.Sprintf("'%s' is not foo. It should be foo. plz make '%s' == foo. plz", str, str)},
}
currentState.AddError(data, fmt.Sprintf("should be foo. plz make '%s' == foo. plz", str))
}
}
return nil
}

func main() {
// register a custom validator by supplying a function
// that creates new instances of your Validator.
jsonschema.RegisterValidator("foo", newIsFoo)
jsonschema.RegisterKeyword("foo", newIsFoo)

schBytes := []byte(`{ "foo": true }`)

// parse a schema that uses your custom keyword
rs := new(jsonschema.RootSchema)
rs := new(Schema)
if err := json.Unmarshal(schBytes, rs); err != nil {
// Real programs handle errors.
panic(err)
}

// validate some JSON
errors := rs.ValidateBytes([]byte(`"bar"`))
errs, err := rs.ValidateBytes([]byte(`"bar"`))
if err != nil {
panic(err)
}

// print le error
fmt.Println(errs[0].Error())

// Output: 'bar' is not foo. It should be foo. plz make 'bar' == foo. plz
// Output: /: "bar" should be foo. plz make 'bar' == foo. plz
}
```

92 changes: 92 additions & 0 deletions draft2019_09_keywords.go
@@ -0,0 +1,92 @@
package jsonschema

// LoadDraft2019_09 loads the keywords for schema validation
// based on draft2019_09
// this is also the default keyword set loaded automatically
// if no other is loaded
func LoadDraft2019_09() {
// core keywords
RegisterKeyword("$schema", NewSchemaURI)
RegisterKeyword("$id", NewID)
RegisterKeyword("description", NewDescription)
RegisterKeyword("title", NewTitle)
RegisterKeyword("$comment", NewComment)
RegisterKeyword("examples", NewExamples)
RegisterKeyword("readOnly", NewReadOnly)
RegisterKeyword("writeOnly", NewWriteOnly)
RegisterKeyword("$ref", NewRef)
RegisterKeyword("$recursiveRef", NewRecursiveRef)
RegisterKeyword("$anchor", NewAnchor)
RegisterKeyword("$recursiveAnchor", NewRecursiveAnchor)
RegisterKeyword("$defs", NewDefs)
RegisterKeyword("default", NewDefault)

SetKeywordOrder("$ref", 0)
SetKeywordOrder("$recursiveRef", 0)

// standard keywords
RegisterKeyword("type", NewType)
RegisterKeyword("enum", NewEnum)
RegisterKeyword("const", NewConst)

// numeric keywords
RegisterKeyword("multipleOf", NewMultipleOf)
RegisterKeyword("maximum", NewMaximum)
RegisterKeyword("exclusiveMaximum", NewExclusiveMaximum)
RegisterKeyword("minimum", NewMinimum)
RegisterKeyword("exclusiveMinimum", NewExclusiveMinimum)

// string keywords
RegisterKeyword("maxLength", NewMaxLength)
RegisterKeyword("minLength", NewMinLength)
RegisterKeyword("pattern", NewPattern)

// boolean keywords
RegisterKeyword("allOf", NewAllOf)
RegisterKeyword("anyOf", NewAnyOf)
RegisterKeyword("oneOf", NewOneOf)
RegisterKeyword("not", NewNot)

// object keywords
RegisterKeyword("properties", NewProperties)
RegisterKeyword("patternProperties", NewPatternProperties)
RegisterKeyword("additionalProperties", NewAdditionalProperties)
RegisterKeyword("required", NewRequired)
RegisterKeyword("propertyNames", NewPropertyNames)
RegisterKeyword("maxProperties", NewMaxProperties)
RegisterKeyword("minProperties", NewMinProperties)
RegisterKeyword("dependentSchemas", NewDependentSchemas)
RegisterKeyword("dependentRequired", NewDependentRequired)
RegisterKeyword("unevaluatedProperties", NewUnevaluatedProperties)

SetKeywordOrder("properties", 2)
SetKeywordOrder("additionalProperties", 3)
SetKeywordOrder("unevaluatedProperties", 4)

// array keywords
RegisterKeyword("items", NewItems)
RegisterKeyword("additionalItems", NewAdditionalItems)
RegisterKeyword("maxItems", NewMaxItems)
RegisterKeyword("minItems", NewMinItems)
RegisterKeyword("uniqueItems", NewUniqueItems)
RegisterKeyword("contains", NewContains)
RegisterKeyword("maxContains", NewMaxContains)
RegisterKeyword("minContains", NewMinContains)
RegisterKeyword("unevaluatedItems", NewUnevaluatedItems)

SetKeywordOrder("maxContains", 2)
SetKeywordOrder("minContains", 2)
SetKeywordOrder("additionalItems", 3)
SetKeywordOrder("unevaluatedItems", 4)

// conditional keywords
RegisterKeyword("if", NewIf)
RegisterKeyword("then", NewThen)
RegisterKeyword("else", NewElse)

SetKeywordOrder("then", 2)
SetKeywordOrder("else", 2)

//optional formats
RegisterKeyword("format", NewFormat)
}
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -3,7 +3,7 @@ module github.com/qri-io/jsonschema
go 1.13

require (
github.com/qri-io/jsonpointer v0.1.0
github.com/qri-io/jsonpointer v0.1.1
github.com/sergi/go-diff v1.0.0
github.com/stretchr/testify v1.3.0 // indirect
)
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qri-io/jsonpointer v0.1.0 h1:OcTtTmorodUCRc2CZhj/ZwOET8zVj6uo0ArEmzoThZI=
github.com/qri-io/jsonpointer v0.1.0/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down

0 comments on commit bb2a1cf

Please sign in to comment.