Skip to content

aarongodin/jsonschema

Repository files navigation

GitHub release CI GitHub

jsonschema

Comprehensive JSON Schema validation and generator for Crystal. Read below for a high-level description of the API, or check out the API Docs for full details.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      jsonschema:
        github: aarongodin/jsonschema
  2. Run shards install

Overview

  • Read JSON files from your file system at compile time and generate Crystal code to convert a JSON Schema document to a JSONSchema::Validator object.
  • Create JSONSchema::Validator instances during runtime by providing JSON input to JSONSchema.from_json()
  • Build JSONSchema::Validator objects using a fluent API directly in your application code.

Usage

Generating Validators

Generate code using the provided macros which output a reference to a JSONSchema::Validator object. You can assign the value to a variable or use it any place an expression can be used.

require "jsonschema"
validator = JSONSchema.create_validator "my_schema.json"

You can use create_validator_method to define a method that returns the reference to the JSONSchema::Validator.

class RequestBody
  JSONSchema.create_validator_method "my_schema.json"
end

This is syntactically equivalent to:

class RequestBody
  def validator : JSONSchema::Validator
    JSONSchema.create_validator "my_schema.json"
  end
end

The create_validator_method macro allows you to also customize the method name that is generated by passing a second argument:

class ExampleRoute
  JSONSchema.create_validator_method "request_schema.json", "request_body_validator"
  JSONSchema.create_validator_method "response_schema.json", "response_body_validator"
end

r = ExampleRoute.new

r.request_body_validator # => #<JSONSchema::ObjectValidator:...
r.response_body_validator # => #<JSONSchema::ObjectValidator:...

Omitting extension: You can omit the file extension in any of the macros, in which case a file with .json as the extension will be loaded. ex:

JSONSchema.create_validator "my_schema" # => Loads "my_schema.json"

Validating JSON

Use the #validate method on any generated validator to receive a JSONSchema::ValidationResult:

require "json"
require "jsonschema"

class RequestBody
  JSONSchema.create_validator_method "my_schema.json"
end

request_body = RequestBody.new
input_json = JSON.parse(%{{"test": "example"}})

request_body.validator.validate(input_json) # => JSONSchema::ValidationResult(@status=:success, @errors=[])

The JSONSchema::ValidationResult will contain either :success or :error as the status. On :error, you can check the @errors array for a list of JSONSchema::ValidationError and respond in your code accordingly.

Context

When a ValidationError is created, it includes a NodeContext object which represents the location where the error occurred. You can get the value from this location by using NodeContext#dig_into:

result = validator.validate(input_json)
result.errors[0].context.dig_into(input_json) # => returns JSON::Any() that wraps value at that location

You can get a string path with NodeContext#to_s similar to what jq (ref) uses for pathing.

result = validator.validate(input_json)
result.errors[0].context.to_s # => String such as ".person.name" or ".example[2].title"

Create From JSON Input

Use the #from_json method to create JSONSchema::Validator objects from any parsed JSON. This is a runtime method and can raise an exception for any invalid schema. To return nil instead of raising, use #from_json?.

validator = JSONSchema.from_json(JSON.parse(
  <<-JSON
    {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    }
  JSON
))

validator.validate(JSON.parse("...")) # => JSONSchema::ValidationResult(@status=:success, @errors=[])

Create From Fluent API

The fluent API is a DSL with a concise syntax for generating JSONSchema::Validator objects. See the Fluent class for full usage specification. Here's an example that shows a complex schema represented using the fluent API:

require "jsonschema"

js = JSONSchema.fluent

validator = js.object do
  prop "first_name", (js.string do
    min_length 2
    max_length 64
  end)

  prop "last_name", (js.string do
    min_length 2
    max_length 64
  end)

  prop "email", js.string { format "email" }

  prop "address", (js.object do
    prop "street", js.string
    prop "city", js.string
    prop "state", js.string
    prop "zipcode", js.string
  end)

  prop "nicknames", (js.array do
    min_items 1
    items js.string
  end)
end

Serialize

You can serialize a validator to its string representation through the standard #to_json methods:

js = JSONSchema.fluent

validator = js.generic do
  any_of(
    js.string,
    js.number
  )
end

puts validator.to_json # {"anyOf":[{"type":"string"},{"type":"number"}]}

json-schema Features

Core Types

All JSON Schema types are supported!

  • string
  • number and integer
  • array
  • object
  • null
  • boolean

The generic keywords const and enum are also supported.

  • enum can be provided either as a generic keyword (schema without a type value), or on a typed schema.
  • const may only be provided as a generic keyword.

Composite Schema

Composite schemas using not, anyOf, allOf, or oneOf are supported! These can be used on any schema, including a generic one with no type.

Conditional Schema

Using dependentRequired, dependentSchemas and if-then-else are supported!

String Formats

JSON Schema provides a number of format keywords that require the validation to restrict the string to values matching the format. This module pulls in crystal-validator to perform most of the validations, and relies on regex for the rest.

Illogical Schema

It's possible to make a JSON schema that is logically impossible. For example, you could create a composite schema that checks that a value is both a number and a string. The generator tries to prevent this from happening. Since the schema is processed through macros, it seems better to present you a compile error that the schema is illogical than to allow you to use a schema that would be additional work to support in the code, but not actually validate anything.

Calling this out here as this is different behavior than most JSON Schema tools out there. Many libraries implement the rules and rely on the user to write a logical schema.

Unsupported

These features of JSON Schema are not yet supported, but will be supported in a future release (at least before 1.0.0).

Dialects

The latest revision of this shard only supports the latest revision of JSON Schema (2020-12). There is not yet support for using a different dialect.

i18n/Translation

You may provide a translation to this module through JSONSchema.i18n. Please see the API docs for documentation.

Acknowledgements

The source for this shard is inspired by the ECR and JSON implementations from the std lib. Thanks to the Crystal team for creating an amazing standard library!