Skip to content

Commit

Permalink
feat(json.Marshaler): marshal schemas back to json properly.
Browse files Browse the repository at this point in the history
this makes jsonschema a 2-way street, marshaling decoded schemas
back to json. Not all test edge cases have been worked out, which
we should cover next, but this'll cover lots & lots of use cases.

I'm enforcing the additional constraint that all schema encoding
be lexographic (by using maps for object encoding), because we're
going to be hashing the results of lots of these schemas,
consistent encoding is vital.

word.
  • Loading branch information
b5 committed Jan 17, 2018
1 parent 05ef7f0 commit f7d8215
Show file tree
Hide file tree
Showing 17 changed files with 365 additions and 83 deletions.
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
[![CI](https://img.shields.io/circleci/project/github/qri-io/jsonschema.svg?style=flat-square)](https://circleci.com/gh/qri-io/jsonschema)
[![Go Report Card](https://goreportcard.com/badge/github.com/qri-io/jsonschema)](https://goreportcard.com/report/github.com/qri-io/jsonschema)

### 🚧🚧 Under Construction 🚧🚧
golang implementation of the [JSON Schema Specification](http://json-schema.org/), which lets you write JSON that validates some other json. Rad.

### 🚧 New Package Alert 🚧
This is a very new implementation and hasn't been tested by things that aren't computers. If you run into issues, please file an... issue, and we'll all work together to address it.

### Package Features

* Encode schemas back to JSON
* Supply Your own Custom Validators
* Uses Standard Go idioms

### Getting Involved

We would love involvement from more people! If you notice any errors or would
Expand Down Expand Up @@ -97,3 +105,55 @@ It involves two steps that should happen _before_ allocating any RootSchema inst
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.


```go
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 {
return new(IsFoo)
}

// Validate implements jsonschema.Validator
func (f IsFoo) Validate(data interface{}) error {
if str, ok := data.(string); ok {
if str != "foo" {
return fmt.Errorf("'%s' is not foo. It should be foo. plz make '%s' == foo. plz", str, str)
}
}
return nil
}

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

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

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

// validate some JSON
err := rs.ValidateBytes([]byte(`"bar"`))

// print le error
fmt.Println(err.Error())

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

38 changes: 22 additions & 16 deletions keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func DataType(data interface{}) string {
// String values MUST be one of the six primitive types ("null", "boolean", "object", "array", "number", or "string"), or
// "integer" which matches any number with a zero fractional part.
// An instance validates if and only if the instance is in any of the sets listed for this keyword.
type tipe []string
type tipe struct {
strVal bool // set to true if tipe decoded from a string, false if an array
vals []string
}

func newTipe() Validator {
return &tipe{}
Expand All @@ -57,17 +60,17 @@ func newTipe() Validator {
// Validate checks to see if input data satisfies the type constraint
func (t tipe) Validate(data interface{}) error {
jt := DataType(data)
for _, typestr := range t {
for _, typestr := range t.vals {
if jt == typestr || jt == "integer" && typestr == "number" {
return nil
}
}
if len(t) == 1 {
return fmt.Errorf(`expected "%v" to be of type %s`, data, t[0])
if len(t.vals) == 1 {
return fmt.Errorf(`expected "%v" to be of type %s`, data, t.vals[0])
}

str := ""
for _, ts := range t {
for _, ts := range t.vals {
str += ts + ","
}
return fmt.Errorf(`expected "%v" to be one of type: %s`, data, str[:len(str)-1])
Expand All @@ -79,27 +82,27 @@ func (t tipe) JSONProp(name string) interface{} {
if err != nil {
return nil
}
if idx > len(t) || idx < 0 {
if idx > len(t.vals) || idx < 0 {
return nil
}
return t[idx]
return t.vals[idx]
}

// UnmarshalJSON implements the json.Unmarshaler interface for tipe
func (t *tipe) UnmarshalJSON(data []byte) error {
var single string
if err := json.Unmarshal(data, &single); err == nil {
*t = tipe{single}
*t = tipe{strVal: true, vals: []string{single}}
} else {
var set []string
if err := json.Unmarshal(data, &set); err == nil {
*t = tipe(set)
*t = tipe{vals: set}
} else {
return err
}
}

for _, pr := range *t {
for _, pr := range t.vals {
if !primitiveTypes[pr] {
return fmt.Errorf(`"%s" is not a valid type`, pr)
}
Expand All @@ -109,12 +112,10 @@ func (t *tipe) UnmarshalJSON(data []byte) error {

// MarshalJSON implements the json.Marshaler interface for tipe
func (t tipe) MarshalJSON() ([]byte, error) {
if len(t) == 1 {
return json.Marshal(t[0])
} else if len(t) > 1 {
return json.Marshal([]string(t))
if t.strVal {
return json.Marshal(t.vals[0])
}
return []byte(`""`), nil
return json.Marshal(t.vals)
}

// enum validates successfully against this keyword if its value is equal to one of the
Expand Down Expand Up @@ -170,7 +171,7 @@ func (e enum) JSONChildren() (res map[string]JSONPather) {
// konst MAY be of any type, including null.
// An instance validates successfully against this keyword if its
// value is equal to the value of the keyword.
type konst []byte
type konst json.RawMessage

func newKonst() Validator {
return &konst{}
Expand Down Expand Up @@ -204,3 +205,8 @@ func (c *konst) UnmarshalJSON(data []byte) error {
*c = data
return nil
}

// MarshalJSON implements json.Marshaler for konst
func (c konst) MarshalJSON() ([]byte, error) {
return json.Marshal(json.RawMessage(c))
}
83 changes: 44 additions & 39 deletions keywords_booleans.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import (
"strconv"
)

// AllOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// allOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against all schemas defined by this keyword's value.
type AllOf []*Schema
type allOf []*Schema

func newAllOf() Validator {
return &AllOf{}
return &allOf{}
}

// Validate implements the validator interface for AllOf
func (a AllOf) Validate(data interface{}) error {
// Validate implements the validator interface for allOf
func (a allOf) Validate(data interface{}) error {
for i, sch := range a {
if err := sch.Validate(data); err != nil {
return fmt.Errorf("allOf element %d error: %s", i, err.Error())
Expand All @@ -24,8 +24,8 @@ func (a AllOf) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for AllOf
func (a AllOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for allOf
func (a allOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -36,26 +36,26 @@ func (a AllOf) JSONProp(name string) interface{} {
return a[idx]
}

// JSONChildren implements the JSONContainer interface for AllOf
func (a AllOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for allOf
func (a allOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range a {
res[strconv.Itoa(i)] = sch
}
return
}

// AnyOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// anyOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against at
// least one schema defined by this keyword's value.
type AnyOf []*Schema
type anyOf []*Schema

func newAnyOf() Validator {
return &AnyOf{}
return &anyOf{}
}

// Validate implements the validator interface for AnyOf
func (a AnyOf) Validate(data interface{}) error {
// Validate implements the validator interface for anyOf
func (a anyOf) Validate(data interface{}) error {
for _, sch := range a {
if err := sch.Validate(data); err == nil {
return nil
Expand All @@ -64,8 +64,8 @@ func (a AnyOf) Validate(data interface{}) error {
return fmt.Errorf("value did not match any specified anyOf schemas: %v", data)
}

// JSONProp implements JSON property name indexing for AnyOf
func (a AnyOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for anyOf
func (a anyOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -76,25 +76,25 @@ func (a AnyOf) JSONProp(name string) interface{} {
return a[idx]
}

// JSONChildren implements the JSONContainer interface for AnyOf
func (a AnyOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for anyOf
func (a anyOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range a {
res[strconv.Itoa(i)] = sch
}
return
}

// OneOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// oneOf MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value.
type OneOf []*Schema
type oneOf []*Schema

func newOneOf() Validator {
return &OneOf{}
return &oneOf{}
}

// Validate implements the validator interface for OneOf
func (o OneOf) Validate(data interface{}) error {
// Validate implements the validator interface for oneOf
func (o oneOf) Validate(data interface{}) error {
matched := false
for _, sch := range o {
if err := sch.Validate(data); err == nil {
Expand All @@ -110,8 +110,8 @@ func (o OneOf) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for OneOf
func (o OneOf) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for oneOf
func (o oneOf) JSONProp(name string) interface{} {
idx, err := strconv.Atoi(name)
if err != nil {
return nil
Expand All @@ -122,26 +122,26 @@ func (o OneOf) JSONProp(name string) interface{} {
return o[idx]
}

// JSONChildren implements the JSONContainer interface for OneOf
func (o OneOf) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for oneOf
func (o oneOf) JSONChildren() (res map[string]JSONPather) {
res = map[string]JSONPather{}
for i, sch := range o {
res[strconv.Itoa(i)] = sch
}
return
}

// Not MUST be a valid JSON Schema.
// not MUST be a valid JSON Schema.
// An instance is valid against this keyword if it fails to validate successfully against the schema defined
// by this keyword.
type Not Schema
type not Schema

func newNot() Validator {
return &Not{}
return &not{}
}

// Validate implements the validator interface for Not
func (n *Not) Validate(data interface{}) error {
// Validate implements the validator interface for not
func (n *not) Validate(data interface{}) error {
sch := Schema(*n)
if sch.Validate(data) == nil {
// TODO - make this error actually make sense
Expand All @@ -150,26 +150,31 @@ func (n *Not) Validate(data interface{}) error {
return nil
}

// JSONProp implements JSON property name indexing for Not
func (n Not) JSONProp(name string) interface{} {
// JSONProp implements JSON property name indexing for not
func (n not) JSONProp(name string) interface{} {
return Schema(n).JSONProp(name)
}

// JSONChildren implements the JSONContainer interface for Not
func (n Not) JSONChildren() (res map[string]JSONPather) {
// JSONChildren implements the JSONContainer interface for not
func (n not) JSONChildren() (res map[string]JSONPather) {
if n.Ref != "" {
s := Schema(n)
return map[string]JSONPather{"$ref": &s}
}
return Schema(n).JSONChildren()
}

// UnmarshalJSON implements the json.Unmarshaler interface for Not
func (n *Not) UnmarshalJSON(data []byte) error {
// UnmarshalJSON implements the json.Unmarshaler interface for not
func (n *not) UnmarshalJSON(data []byte) error {
var sch Schema
if err := json.Unmarshal(data, &sch); err != nil {
return err
}
*n = Not(sch)
*n = not(sch)
return nil
}

// MarshalJSON implements json.Marshaller for not
func (n not) MarshalJSON() ([]byte, error) {
return json.Marshal(Schema(n))
}
Loading

0 comments on commit f7d8215

Please sign in to comment.