Skip to content

Instantly share code, notes, and snippets.

@croaker
Last active March 12, 2020 13:30
Show Gist options
  • Save croaker/a2a27ba7ba61efdeb566a59047ce756e to your computer and use it in GitHub Desktop.
Save croaker/a2a27ba7ba61efdeb566a59047ce756e to your computer and use it in GitHub Desktop.
A JsonValidator for ActiveModel using json-schema

JsonValidator

A simple validator to validate jsonb columns against a JSON schema. It's dependent on the json-schema gem, make sure to add gem 'json-schema' to your Gemfile and run $ bundle.

To use this, add json_validator.rb to app/models/concerns and json_validator_spec.rb to spec/models/concerns. By convention, schemas are stored in app/models/json_schemas.

Example

The following example illustrates a model Post that has a tags attribute. Using the JsonValidator and the tags_attribute.json schema it ensures that tags is only valid if it's an array of strings.

The Post model in app/models/post.rb

class Post < ApplicationRecord
  validates_with Validators::JsonValidator, attribute: :tags, schema: :tags_attribute
end

Tags attribute schema in app/models/json_schemas/tags_attribute.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Tags",
  "description": "A list of tags",
  "type": "array",
  "items": {
    "type": "string"
  }
}

Note: You can omit $schema, title, and description.

en:
errors:
messages:
invalid_json_schema: does not match the expected schema
module Validators
class JsonValidator < ActiveModel::Validator
attr_reader :attribute, :schema, :schema_root
def initialize(options = {})
super
@attribute = options.fetch(:attribute)
@schema_root = options.fetch(:schema_root) { default_schema_root }
@schema = prepare_schema(options.fetch(:schema))
end
def validate(record)
unless valid_json?(record.send(attribute))
record.errors.add(attribute, :invalid_json_schema)
end
end
private
def valid_json?(text)
JSON::Validator.validate(schema, text)
end
def prepare_schema(schema)
schema_path = File.join(schema_root, "#{schema}.json")
File.read(schema_path)
rescue Errno::ENOENT
schema
end
def default_schema_root
Rails.root + "app/models/json_schemas"
end
end
end
require "spec_helper"
require "active_model"
require "json-schema"
require_relative "../../../app/models/concerns/validators/json_validator"
RSpec.describe Validators::JsonValidator do
class Validatable
include ActiveModel::Validations
def initialize(attrs = {})
@data = attrs[:data].to_json
end
attr_accessor :data
end
describe "options" do
it "accepts attribute, schema_root, and schema" do
expect {
Validators::JsonValidator.new(
attribute: "data", schema_root: "/", schema: {}
)
}.not_to raise_error
end
context "without attribute" do
it "raises an error" do
expect {
Validators::JsonValidator.new(schema_root: "/", schema: {})
}.to raise_error(KeyError)
end
end
context "without schema" do
it "raises an error" do
expect {
Validators::JsonValidator.new(attribute: "data", schema_root: "/")
}.to raise_error(KeyError)
end
end
context "without schema_root" do
it "defaults to default_schema_root" do
stub_const "Rails", double(root: "/")
validator = Validators::JsonValidator.new(attribute: "data", schema: {})
expect(validator.schema_root)
.to eq("/app/models/json_schemas")
end
end
describe "schema" do
context "as file" do
it "loads the schema json from schema_root" do
expect(File).to receive(:read).with("/schema_root/some_schema.json")
.and_return('{ "type": "string" }')
validator = Validators::JsonValidator.new(attribute: "data",
schema_root: "/schema_root",
schema: :some_schema)
expect(validator.schema).to eq('{ "type": "string" }')
end
end
context "not a file" do
it "falls back to schema" do
validator = Validators::JsonValidator.new(
attribute: "data",
schema_root: "/i/dont/exists",
schema: { type: "object" }
)
expect(validator.schema).to eq({ type: "object" })
end
end
end
end
describe "#validate" do
let(:validator) {
Validators::JsonValidator.new(attribute: "data",
schema_root: "/arbitrary/path",
schema: { type: "object" })
}
context "with valid data" do
it "does not add an error to the field" do
record = Validatable.new(data: {})
validator.validate(record)
expect(record.errors[:data]).to be_empty
end
end
context "with invalid data" do
it "adds an error to the field" do
record = Validatable.new(data: 'invalid')
validator.validate(record)
expect(record.errors[:data]).to include(
I18n.t("errors.messages.invalid_json_schema")
)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment