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

refactor(jsonschema): reworking how we handle json schema #65

Merged
merged 7 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
coverage.txt
coverage.txt
.vscode
140 changes: 82 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
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())
}
"title": "Person",
"type": "object",
"$id": "https://qri.io/schema/",
"$comment" : "sample comment",
Arqu marked this conversation as resolved.
Show resolved Hide resolved
"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 draf keyword set (see `draft2019_09_keywords.go`)
Arqu marked this conversation as resolved.
Show resolved Hide resolved
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 {
if str, ok := data.(string); ok {
// 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
}

// ValidateFromContext implements jsonschema.Keyword
func (f *IsFoo) ValidateFromContext(schCtx *SchemaContext, errs *[]KeyError) {
if str, ok := schCtx.Instance.(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)},
}
AddErrorCtx(errs, schCtx, 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
}
```

88 changes: 88 additions & 0 deletions draft2019_09_keywords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

// 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)

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

// 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)

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

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

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

//optional formats
RegisterKeyword("format", NewFormat)
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module github.com/qri-io/jsonschema
go 1.13

require (
github.com/qri-io/jsonpointer v0.1.0
github.com/pkg/profile v1.4.0
Arqu marked this conversation as resolved.
Show resolved Hide resolved
github.com/qri-io/jsonpointer v0.1.1
github.com/sergi/go-diff v1.0.0
github.com/stretchr/testify v1.3.0 // indirect
)
Loading