jtd

package module
v0.0.0-...-4280071 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2020 License: MIT Imports: 5 Imported by: 7

README

jtd: JSON Validation for Golang

GoDoc Badge

This package implements JSON Typedef validation for Golang. If you're trying to do JSON Typedef code generation, see "Generating Golang from JSON Typedef Schemas" in the JSON Typedef docs.

jtd is a Golang implementation of JSON Type Definition, a schema language for JSON. jtd primarily gives you two things:

  1. Validating input data against JSON Typedef schemas.
  2. A Golang representation of JSON Typedef schemas.

With this package, you can add JSON Typedef-powered validation to your application, or you can build your own tooling on top of JSON Type Definition.

Installation

If you're using Go modules, install this package by running:

go get github.com/jsontypedef/json-typedef-go

Although the package's name ends in json-typedef-go, it exposes a package called jtd. In other words, this:

import "github.com/jsontypedef/json-typedef-go"

Is the same thing as:

import jtd "github.com/jsontypedef/json-typedef-go"

Documentation

Detailed API documentation is available online at:

https://godoc.org/github.com/jsontypedef/json-typedef-go

For more high-level documentation about JSON Typedef in general, or JSON Typedef in combination with Golang in particular, see:

Basic Usage

For a more detailed tutorial and guidance on how to integrate jtd in your application, see "Validating JSON in Golang with JSON Typedef" in the JSON Typedef docs.

Here's an example of how you can use this package to validate JSON data against a JSON Typedef schema:

package main

import (
	"encoding/json"
	"fmt"

	jtd "github.com/jsontypedef/json-typedef-go"
)

func main() {
	schemaJSON := `{
		"properties": {
			"name": { "type": "string" },
			"age": { "type": "uint32" },
			"phones": {
				"elements": { "type": "string" }
			}
		}
	}`

	var schema jtd.Schema
	json.Unmarshal([]byte(schemaJSON), &schema)

	// jtd.Validate returns an array of validation errors. If there were no
	// problems with the input, it returns an empty array.

	// This input is perfect, so we'll get back an empty list of validation
	// errors.
	okJSON := `{
		"name": "John Doe",
		"age": 43,
		"phones": ["+44 1234567", "+44 2345678"]
	}`

	var ok interface{}
	json.Unmarshal([]byte(okJSON), &ok)

	// Outputs:
	// [] <nil>
	fmt.Println(jtd.Validate(schema, ok))

	// This next input has three problems with it:
	//
	// 1. It's missing "name", which is a required property.
	// 2. "age" is a string, but it should be an integer.
	// 3. "phones[1]" is a number, but it should be a string.
	//
	// Each of those errors corresponds to one of the errors returned by
	// jtd.Validate.
	badJSON := `{
		"age": "43",
		"phones": ["+44 1234567", 442345678]
	}`

	var bad interface{}
	json.Unmarshal([]byte(badJSON), &bad)

	// Outputs something like (order may change):
	//
	// []jtd.ValidateError{
	// 	jtd.ValidateError{
	// 		InstancePath: []string{},
	// 		SchemaPath: []string{"properties", "name"}
	// 	},
	// 	jtd.ValidateError{
	// 		InstancePath: []string{"age"},
	// 		SchemaPath: []string{"properties", "age", "type"}
	// 	},
	// 	jtd.ValidateError{
	// 		InstancePath: []string{"phones", "1"},
	// 		SchemaPath: []string{"properties", "phones", "elements", "type"}
	// 	}
	// }
	errs, _ := jtd.Validate(schema, bad)
	fmt.Printf("%#v\n", errs)
}

Advanced Usage: Limiting Errors Returned

By default, jtd.Validate returns every error it finds. If you just care about whether there are any errors at all, or if you can't show more than some number of errors, then you can get better performance out of jtd.Validate using the WithMaxErrors option.

For example, taking the same example from before, but limiting it to 1 error, we get:

// []jtd.ValidateError{
// 	jtd.ValidateError{
// 		InstancePath: []string{},
// 		SchemaPath: []string{"properties", "name"}
// 	}
// }
errs, _ := jtd.Validate(schema, bad, jtd.WithMaxErrors(1))
fmt.Printf("%#v\n", errs)

Advanced Usage: Handling Untrusted Schemas

If you want to run jtd against a schema that you don't trust, then you should:

  1. Ensure the schema is well-formed, using the Validate method on Schema, which validates things like making sure all refs have corresponding definitions.

  2. Call jtd.Validate with the WithMaxDepth option. JSON Typedef lets you write recursive schemas -- if you're evaluating against untrusted schemas, you might go into an infinite loop when evaluating against a malicious input, such as this one:

    {
      "ref": "loop",
      "definitions": {
        "loop": {
          "ref": "loop"
        }
      }
    }
    

    The MaxDepth option tells jtd.Validate how many refs to follow recursively before giving up and throwing jtd.ErrMaxDepthExceeded.

Here's an example of how you can use jtd to evaluate data against an untrusted schema:

func validateUntrusted(schema jtd.Schema, instance interface{}) (bool, error) {
	if err := schema.Validate(); err != nil {
		return false, err
	}

	// You should tune WithMaxDepth to be high enough that most legitimate schemas
	// evaluate without errors, but low enough that an attacker cannot cause a
	// denial of service attack.
	errs, err := jtd.Validate(schema, instance, jtd.WithMaxDepth(32))
	if err != nil {
		return false, err
	}

	return len(errs) == 0, nil
}

// Returns true
validateUntrusted(jtd.Schema{Type: jtd.TypeString}, "foo")

// Returns false
validateUntrusted(jtd.Schema{Type: jtd.TypeString}, nil)

// Returns jtd.ErrInvalidType
validateUntrusted(jtd.Schema{Type: "nonsense"}, nil)

// Returns jtd.ErrMaxDepthExceeded
loop := "loop"
validateUntrusted(jtd.Schema{
	Definitions: map[string]jtd.Schema{
		"loop": jtd.Schema{
			Ref: &loop,
		},
	},
	Ref: &loop,
}, nil)

Documentation

Index

Examples

Constants

View Source
const (
	// TypeBoolean represents true or false.
	TypeBoolean Type = "boolean"

	// TypeFloat32 represents a JSON number. Code generators will create a
	// single-precision floating point from this.
	TypeFloat32 = "float32"

	// TypeFloat64 represents a JSON number. Code generators will create a
	// double-precision floating point from this.
	TypeFloat64 = "float64"

	// TypeInt8 represents a JSON number within the range of a int8.
	TypeInt8 = "int8"

	// TypeUint8 represents a JSON number within the range of a uint8.
	TypeUint8 = "uint8"

	// TypeInt16 represents a JSON number within the range of a int16.
	TypeInt16 = "int16"

	// TypeUint16 represents a JSON number within the range of a uint16.
	TypeUint16 = "uint16"

	// TypeInt32 represents a JSON number within the range of a int32.
	TypeInt32 = "int32"

	// TypeUint32 represents a JSON number within the range of a uint32.
	TypeUint32 = "uint32"

	// TypeString represents a JSON string.
	TypeString = "string"

	// TypeTimestamp represents a JSON string containing a RFC3339 timestamp.
	TypeTimestamp = "timestamp"
)
View Source
const (
	// FormEmpty is the empty form.
	FormEmpty Form = "empty"

	// FormRef is the ref form.
	FormRef = "ref"

	// FormType is the type form.
	FormType = "type"

	// FormEnum is the enum form.
	FormEnum = "enum"

	// FormElements is the elements form.
	FormElements = "elements"

	// FormProperties is the properties form.
	FormProperties = "properties"

	// FormValues is the values form.
	FormValues = "values"

	// FormDiscriminator is the discriminator form.
	FormDiscriminator = "discriminator"
)

Variables

View Source
var ErrEmptyEnum = errors.New("jtd: empty enum")

ErrEmptyEnum indicates that a schema has a "enum" keyword with no values.

View Source
var ErrInvalidForm = errors.New("jtd: invalid form")

ErrInvalidForm indicates that a schema uses an invalid combination of keywords.

View Source
var ErrInvalidType = errors.New("jtd: invalid type")

ErrInvalidType indicates that a schema has a "type" keyword with an invalid value.

View Source
var ErrMappingRepeatedDiscriminator = errors.New("jtd: mapping re-specifies discriminator property")

ErrMappingRepeatedDiscriminator indicates that a schema has a mapping value that has the same property as the discriminator it's within.

View Source
var ErrMaxDepthExceeded = errors.New("jtd: max depth exceeded")

ErrMaxDepthExceeded is the error returned from Validate if too many refs are recursively followed.

The maximum depth of refs to follow is controlled by MaxErrors in ValidateSettings.

View Source
var ErrNoSuchDefinition = errors.New("jtd: ref to non-existent definition")

ErrNoSuchDefinition indicates that a schema has a "ref" with no corresponding definition.

View Source
var ErrNonPropertiesMapping = errors.New("jtd: mapping value not of properties form")

ErrNonPropertiesMapping indicates that a schema has a mapping value that isn't a schema of the properties form.

View Source
var ErrNonRootDefinition = errors.New("jtd: non-root definitions")

ErrNonRootDefinition indicates that a schema uses the "definition" keyword outside of a root schema.

View Source
var ErrNullableMapping = errors.New("jtd: mapping allows for nullable values")

ErrNullableMapping indicates that a schema has a mapping value with "nullable" set to true.

View Source
var ErrRepeatedEnumValue = errors.New("jtd: enum contains repeated values")

ErrRepeatedEnumValue indicates that a schema has a "enum" keyword with repeated values.

View Source
var ErrSharedProperty = errors.New("jtd: properties and optionalProperties share property")

ErrSharedProperty indicates that a schema has the same property name in "properties" and "optionalProperties".

Functions

This section is empty.

Types

type Form

type Form string

Form is an enumeration of the eight forms a JSON Typedef schema may take on.

type Schema

type Schema struct {
	Definitions          map[string]Schema      `json:"definitions"`
	Metadata             map[string]interface{} `json:"metadata"`
	Nullable             bool                   `json:"nullable"`
	Ref                  *string                `json:"ref"`
	Type                 Type                   `json:"type"`
	Enum                 []string               `json:"enum"`
	Elements             *Schema                `json:"elements"`
	Properties           map[string]Schema      `json:"properties"`
	OptionalProperties   map[string]Schema      `json:"optionalProperties"`
	AdditionalProperties bool                   `json:"additionalProperties"`
	Values               *Schema                `json:"values"`
	Discriminator        string                 `json:"discriminator"`
	Mapping              map[string]Schema      `json:"mapping"`
}

Schema represents a JSON Typedef Schema.

func (Schema) Form

func (s Schema) Form() Form

Form returns JSON Typedef schema form that s takes on.

func (Schema) Validate

func (s Schema) Validate() error

Validate returns an error if a schema is not a valid root JSON Typedef schema.

Validate may return one of ErrInvalidForm, ErrNonRootDefinition, ErrNoSuchDefinition, ErrInvalidType, ErrEmptyEnum, ErrRepeatedEnumValue, ErrSharedProperty, ErrNonPropertiesMapping, ErrMappingRepeatedDiscriminator, or ErrNullableMapping.

func (Schema) ValidateWithRoot

func (s Schema) ValidateWithRoot(isRoot bool, root Schema) error

ValidateWithRoot returns an error if s is not a valid schema, given the root schema s is supposed to appear within.

isRoot indicates whether the schema is expected to be a root schema. root is the root schema s is supposed to be contained within. If isRoot is true, then root should be equal to s for the return value to be meaningful.

type Type

type Type string

Type represents the values that the JSON Typedef "type" keyword can take on.

type ValidateError

type ValidateError struct {
	// Path to the part of the instance that was invalid.
	InstancePath []string

	// Path to the part of the schema that rejected the instance.
	SchemaPath []string
}

ValidateError is a validation error returned from Validate.

This corresponds to a standard error indicator from the JSON Typedef specification.

func Validate

func Validate(schema Schema, instance interface{}, opts ...ValidateOption) ([]ValidateError, error)

Validate validates a schema against an instance (or "input").

Returns ErrMaxDepthExceeded if too many refs are recursively followed while validating. Otherwise, returns a set of ValidateError, in conformance with the JSON Typedef specification.

Example
package main

import (
	"encoding/json"
	"fmt"

	jtd "github.com/jsontypedef/json-typedef-go"
)

func main() {
	var schema jtd.Schema
	json.Unmarshal([]byte(`{
		"properties": {
			"name": { "type": "string" },
			"age": { "type": "uint32" },
			"phones": {
				"elements": { "type": "string" }
			}
		}
	}`), &schema)

	var dataOk interface{}
	json.Unmarshal([]byte(`{
		"name": "John Doe",
		"age": 43,
		"phones": ["+44 1234567", "+44 2345678"]
	}`), &dataOk)

	fmt.Println(jtd.Validate(schema, dataOk))

	var dataBad interface{}
	json.Unmarshal([]byte(`{
		"name": "John Doe",
		"age": 43,
		"phones": ["+44 1234567", 442345678]
	}`), &dataBad)

	fmt.Println(jtd.Validate(schema, dataBad))

}
Output:

[] <nil>
[{[phones 1] [properties phones elements type]}] <nil>
Example (MaxDepth)
package main

import (
	"fmt"

	jtd "github.com/jsontypedef/json-typedef-go"
)

func main() {
	loop := "loop"
	schema := jtd.Schema{
		Definitions: map[string]jtd.Schema{
			"loop": jtd.Schema{
				Ref: &loop,
			},
		},
		Ref: &loop,
	}

	// If you ran this, you would overflow the stack:
	// jtd.Validate(schema, nil)

	fmt.Println(jtd.Validate(schema, nil, jtd.WithMaxDepth(32)))
}
Output:

[] jtd: max depth exceeded
Example (MaxErrors)
package main

import (
	"fmt"

	jtd "github.com/jsontypedef/json-typedef-go"
)

func main() {
	schema := jtd.Schema{
		Elements: &jtd.Schema{
			Type: jtd.TypeBoolean,
		},
	}

	instance := []interface{}{nil, nil, nil, nil, nil}

	fmt.Println(jtd.Validate(schema, instance))
	fmt.Println(jtd.Validate(schema, instance, jtd.WithMaxErrors(3)))
}
Output:

[{[0] [elements type]} {[1] [elements type]} {[2] [elements type]} {[3] [elements type]} {[4] [elements type]}] <nil>
[{[0] [elements type]} {[1] [elements type]} {[2] [elements type]}] <nil>

func ValidateWithSettings

func ValidateWithSettings(settings ValidateSettings, schema Schema, instance interface{}) ([]ValidateError, error)

ValidateWithSettings validates a schema against an instance, using a set of settings.

Returns ErrMaxDepthExceeded if too many refs are recursively followed while validating. Otherwise, returns a set of ValidateError, in conformance with the JSON Typedef specification.

type ValidateOption

type ValidateOption func(*ValidateSettings)

ValidateOption is an option you can pass to Validate.

func WithMaxDepth

func WithMaxDepth(maxDepth int) ValidateOption

WithMaxDepth sets the the MaxDepth option of ValidateSettings.

func WithMaxErrors

func WithMaxErrors(maxErrors int) ValidateOption

WithMaxErrors sets the the MaxErrors option of ValidateSettings.

type ValidateSettings

type ValidateSettings struct {
	// The maximum number of refs to recursively follow before returning
	// ErrMaxDepthExceeded. Zero disables a max depth altogether.
	MaxDepth int

	// The maximum number of validation errors to return. Zero disables a max
	// number of errors altogether.
	MaxErrors int
}

ValidateSettings are settings that configure ValidateWithSettings.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL