├── test ├── data │ ├── good_data_1.json │ ├── bad_data_1.json │ ├── all_of_ref_data.json │ ├── any_of_ref_data.json │ └── one_of_ref_links_data.json ├── schemas │ ├── all_of_ref_base_schema.json │ ├── any_of_ref_jane_schema.json │ ├── any_of_ref_jimmy_schema.json │ ├── any_of_ref_john_schema.json │ ├── good_schema_extends1.json │ ├── good_schema_1.json │ ├── good_schema_2.json │ ├── all_of_ref_schema.json │ ├── relative_definition_schema.json │ ├── good_schema_extends2.json │ ├── ref john with spaces schema.json │ ├── definition_schema_with_special_characters.json │ ├── definition_schema.json │ ├── inner_schema.json │ ├── self_link_schema.json │ ├── up_link_schema.json │ ├── one_of_ref_links_schema.json │ ├── any_of_ref_schema.json │ ├── extends_and_patternProperties_schema.json │ ├── address_microformat.json │ └── extends_and_additionalProperties_false_schema.json ├── min_items_test.rb ├── relative_definition_test.rb ├── list_option_test.rb ├── type_attribute_test.rb ├── all_of_ref_schema_test.rb ├── load_ref_schema_test.rb ├── caching_test.rb ├── stringify_test.rb ├── ruby_schema_test.rb ├── extends_nested_test.rb ├── support │ ├── test_helper.rb │ ├── object_validation.rb │ ├── number_validation.rb │ ├── type_validation.rb │ ├── strict_validation.rb │ ├── enum_validation.rb │ ├── array_validation.rb │ └── string_validation.rb ├── bad_schema_ref_test.rb ├── any_of_ref_schema_test.rb ├── validator_schema_reader_test.rb ├── merge_missing_values_test.rb ├── uri_parsing_test.rb ├── fragment_validation_with_ref_test.rb ├── common_test_suite_test.rb ├── fragment_resolution_test.rb ├── files_test.rb ├── extended_schema_test.rb ├── schema_reader_test.rb ├── draft2_test.rb ├── one_of_test.rb ├── draft1_test.rb ├── initialize_data_test.rb ├── schema_validation_test.rb ├── full_validation_test.rb └── custom_format_test.rb ├── VERSION.yml ├── .gitignore ├── gemfiles ├── Gemfile.multi_json.x ├── Gemfile.uuidtools.x ├── Gemfile.yajl-ruby.x └── Gemfile.ruby_19.x ├── lib ├── json-schema │ ├── errors │ │ ├── uri_error.rb │ │ ├── schema_error.rb │ │ ├── json_load_error.rb │ │ ├── json_parse_error.rb │ │ ├── custom_format_error.rb │ │ ├── schema_parse_error.rb │ │ └── validation_error.rb │ ├── attributes │ │ ├── multipleof.rb │ │ ├── dependencies_v4.rb │ │ ├── limits │ │ │ ├── maximum_inclusive.rb │ │ │ ├── minimum_inclusive.rb │ │ │ ├── items.rb │ │ │ ├── length.rb │ │ │ ├── properties.rb │ │ │ ├── maximum.rb │ │ │ ├── minimum.rb │ │ │ ├── max_items.rb │ │ │ ├── max_length.rb │ │ │ ├── min_items.rb │ │ │ ├── min_length.rb │ │ │ ├── max_properties.rb │ │ │ ├── min_properties.rb │ │ │ └── numeric.rb │ │ ├── properties_v4.rb │ │ ├── disallow.rb │ │ ├── format.rb │ │ ├── uniqueitems.rb │ │ ├── formats │ │ │ ├── date_time_v4.rb │ │ │ ├── uri.rb │ │ │ ├── custom.rb │ │ │ ├── date.rb │ │ │ ├── time.rb │ │ │ ├── ip.rb │ │ │ └── date_time.rb │ │ ├── pattern.rb │ │ ├── maxdecimal.rb │ │ ├── divisibleby.rb │ │ ├── patternproperties.rb │ │ ├── enum.rb │ │ ├── items.rb │ │ ├── type_v4.rb │ │ ├── properties_optional.rb │ │ ├── required.rb │ │ ├── additionalitems.rb │ │ ├── not.rb │ │ ├── limit.rb │ │ ├── dependencies.rb │ │ ├── allof.rb │ │ ├── extends.rb │ │ ├── anyof.rb │ │ ├── oneof.rb │ │ ├── additionalproperties.rb │ │ ├── ref.rb │ │ ├── properties.rb │ │ └── type.rb │ ├── validators │ │ ├── hyper-draft1.rb │ │ ├── hyper-draft2.rb │ │ ├── hyper-draft3.rb │ │ ├── hyper-draft4.rb │ │ ├── hyper-draft6.rb │ │ ├── draft1.rb │ │ ├── draft2.rb │ │ ├── draft3.rb │ │ ├── draft6.rb │ │ └── draft4.rb │ ├── util │ │ ├── array_set.rb │ │ └── uri.rb │ ├── schema │ │ ├── validator.rb │ │ └── reader.rb │ ├── attribute.rb │ └── schema.rb └── json-schema.rb ├── Gemfile ├── .gitmodules ├── .travis.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── json-schema.gemspec ├── LICENSE.md ├── Rakefile ├── CHANGELOG.md └── resources ├── draft-01.json ├── draft-02.json ├── draft-03.json ├── draft-06.json └── draft-04.json /test/data/good_data_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "a" : 5 3 | } -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | major: 2 2 | minor: 8 3 | patch: 1 4 | -------------------------------------------------------------------------------- /test/data/bad_data_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "a" : "poop" 3 | } -------------------------------------------------------------------------------- /test/data/all_of_ref_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "john" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | .*.swp 3 | pkg 4 | *.gem 5 | /Gemfile.lock 6 | .bundle 7 | .idea 8 | /coverage 9 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.multi_json.x: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec :path => "../" 4 | 5 | gem "multi_json" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.uuidtools.x: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec :path => "../" 4 | 5 | gem "uuidtools" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.yajl-ruby.x: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec :path => "../" 4 | 5 | gem "yajl-ruby" 6 | -------------------------------------------------------------------------------- /test/data/any_of_ref_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "names" : 3 | [ "john" 4 | , "jane" 5 | , "jimmy" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /lib/json-schema/errors/uri_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class UriError < StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/json-schema/errors/schema_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class SchemaError < StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/schemas/all_of_ref_base_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties" : { 4 | "name" : { "type": "integer" } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "json", ">= 1.7", :platforms => :mri_19 6 | gem 'simplecov', :require => false 7 | -------------------------------------------------------------------------------- /lib/json-schema/errors/json_load_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class JsonLoadError < StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/json-schema/errors/json_parse_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class JsonParseError < StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/json-schema/errors/custom_format_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class CustomFormatError < StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/schemas/any_of_ref_jane_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema" : "http://json-schema.org/draft-04/schema#" 2 | , "type" : "string" 3 | , "pattern" : "jane" 4 | } 5 | -------------------------------------------------------------------------------- /test/schemas/any_of_ref_jimmy_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema" : "http://json-schema.org/draft-04/schema#" 2 | , "type" : "string" 3 | , "pattern" : "jimmy" 4 | } 5 | -------------------------------------------------------------------------------- /test/schemas/any_of_ref_john_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema" : "http://json-schema.org/draft-04/schema#" 2 | , "type" : "string" 3 | , "pattern" : "john" 4 | } 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/test-suite"] 2 | path = test/test-suite 3 | branch = master 4 | url = git://github.com/json-schema-org/JSON-Schema-Test-Suite.git 5 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.ruby_19.x: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec :path => "../" 4 | 5 | gem "json", "~> 1.0" 6 | gem "addressable", "< 2.5" 7 | gem "webmock", "< 3" 8 | -------------------------------------------------------------------------------- /test/schemas/good_schema_extends1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "object", 3 | "extends": {"$ref": "good_schema_1.json"}, 4 | "properties" : { 5 | "c" : { 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/json-schema/errors/schema_parse_error.rb: -------------------------------------------------------------------------------- 1 | require 'json/common' 2 | 3 | module JSON 4 | class Schema 5 | class SchemaParseError < JSON::ParserError 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/schemas/good_schema_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "object", 3 | "properties" : { 4 | "a" : { 5 | "type" : "integer" 6 | } 7 | }, 8 | "required": ["a"] 9 | } 10 | -------------------------------------------------------------------------------- /test/schemas/good_schema_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "object", 3 | "properties" : { 4 | "b" : { 5 | "$ref" : "good_schema_1.json" 6 | } 7 | }, 8 | "required": ["b"] 9 | } 10 | -------------------------------------------------------------------------------- /test/schemas/all_of_ref_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema" : "http://json-schema.org/draft-04/schema#", 2 | "type" : "object", 3 | "allOf" : 4 | [ { "$ref" : "all_of_ref_base_schema.json" } 5 | ] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /test/data/one_of_ref_links_data.json: -------------------------------------------------------------------------------- 1 | { "links": 2 | [{ "rel" : ["self"] , "href":"http://api.example.com/api/object/3" } 3 | ,{ "rel" : ["up"] , "href":"http://api.example.com/api/object" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/schemas/relative_definition_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "properties": { 4 | "a": { 5 | "$ref": "definition_schema.json#/definitions/foo" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/schemas/good_schema_extends2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "object", 3 | "extends": [ 4 | {"$ref": "good_schema_1.json"}, 5 | {"$ref": "good_schema_2.json"} 6 | ], 7 | "properties" : { 8 | "c" : { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/multipleof.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/divisibleby' 2 | 3 | module JSON 4 | class Schema 5 | class MultipleOfAttribute < DivisibleByAttribute 6 | def self.keyword 7 | 'multipleOf' 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/schemas/ref john with spaces schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema" : "http://json-schema.org/draft-04/schema#", 3 | "type" : "object", 4 | "required" : ["first"], 5 | "properties": { 6 | "first": { 7 | "type": "string", 8 | "enum": ["john"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/dependencies_v4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/dependencies' 2 | 3 | module JSON 4 | class Schema 5 | class DependenciesV4Attribute < DependenciesAttribute 6 | def self.accept_value?(value) 7 | value.is_a?(Array) || value.is_a?(Hash) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/maximum_inclusive.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/maximum' 2 | 3 | module JSON 4 | class Schema 5 | class MaximumInclusiveAttribute < MaximumAttribute 6 | def self.exclusive?(schema) 7 | schema['maximumCanEqual'] == false 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/minimum_inclusive.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/minimum' 2 | 3 | module JSON 4 | class Schema 5 | class MinimumInclusiveAttribute < MinimumAttribute 6 | def self.exclusive?(schema) 7 | schema['minimumCanEqual'] == false 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/schemas/definition_schema_with_special_characters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "a": { 6 | "$ref": "#/definitions/foo:bar" 7 | } 8 | }, 9 | "definitions": { 10 | "foo:bar": { 11 | "type": "integer" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/items.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limit' 2 | 3 | module JSON 4 | class Schema 5 | class ItemsLimitAttribute < LimitAttribute 6 | def self.acceptable_type 7 | Array 8 | end 9 | 10 | def self.value(data) 11 | data.length 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/length.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limit' 2 | 3 | module JSON 4 | class Schema 5 | class LengthLimitAttribute < LimitAttribute 6 | def self.acceptable_type 7 | String 8 | end 9 | 10 | def self.value(data) 11 | data.length 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft1.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | 4 | class HyperDraft1 < Draft1 5 | def initialize 6 | super 7 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-01/hyper-schema#") 8 | end 9 | 10 | JSON::Validator.register_validator(self.new) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft2.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | 4 | class HyperDraft2 < Draft2 5 | def initialize 6 | super 7 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-02/hyper-schema#") 8 | end 9 | 10 | JSON::Validator.register_validator(self.new) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft3.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | 4 | class HyperDraft3 < Draft3 5 | def initialize 6 | super 7 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-03/hyper-schema#") 8 | end 9 | 10 | JSON::Validator.register_validator(self.new) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft4.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | 4 | class HyperDraft4 < Draft4 5 | def initialize 6 | super 7 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-04/hyper-schema#") 8 | end 9 | 10 | JSON::Validator.register_validator(self.new) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft6.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | 4 | class HyperDraft6 < Draft6 5 | def initialize 6 | super 7 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-06/hyper-schema#") 8 | end 9 | 10 | JSON::Validator.register_validator(self.new) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/properties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limit' 2 | 3 | module JSON 4 | class Schema 5 | class PropertiesLimitAttribute < LimitAttribute 6 | def self.acceptable_type 7 | Hash 8 | end 9 | 10 | def self.value(data) 11 | data.size 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/schemas/definition_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "description": "schema with definition", 4 | "type": "object", 5 | "properties": { 6 | "a": { 7 | "$ref": "#/definitions/foo" 8 | } 9 | }, 10 | "definitions": { 11 | "foo": { 12 | "type": "integer" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/schemas/inner_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "innerA": { 5 | "description": "blah", 6 | "type":"boolean" 7 | }, 8 | "innerB": { 9 | "description": "blah", 10 | "type":"boolean" 11 | }, 12 | "innerC": { 13 | "description": "blah", 14 | "type": "boolean" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/schemas/self_link_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema": "http://json-schema.org/draft-04/schema#" 2 | , "type": "object" 3 | , "properties" : 4 | { "rel" : 5 | { "type" : "array" 6 | , "items" : 7 | [ { "type" : "string" 8 | , "pattern" : "self" 9 | } 10 | ] 11 | } 12 | , "href" : 13 | { "type" : "string" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test/schemas/up_link_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema": "http://json-schema.org/draft-04/schema#" 2 | , "type": "object" 3 | , "properties" : 4 | { "rel" : 5 | { "type" : "array" 6 | , "items" : 7 | [ { "type" : "string" 8 | , "pattern" : "up" 9 | } 10 | ] 11 | } 12 | , "href" : 13 | { "type" : "string" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/maximum.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/numeric' 2 | 3 | module JSON 4 | class Schema 5 | class MaximumAttribute < NumericLimitAttribute 6 | def self.limit_name 7 | 'maximum' 8 | end 9 | 10 | def self.exclusive?(schema) 11 | schema['exclusiveMaximum'] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/minimum.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/numeric' 2 | 3 | module JSON 4 | class Schema 5 | class MinimumAttribute < NumericLimitAttribute 6 | def self.limit_name 7 | 'minimum' 8 | end 9 | 10 | def self.exclusive?(schema) 11 | schema['exclusiveMinimum'] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/max_items.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/items' 2 | 3 | module JSON 4 | class Schema 5 | class MaxItemsAttribute < ItemsLimitAttribute 6 | def self.limit_name 7 | 'maxItems' 8 | end 9 | 10 | def self.error_message(schema) 11 | "had more items than the allowed #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/schemas/one_of_ref_links_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema": "http://json-schema.org/draft-04/schema#" 2 | , "type": "object" 3 | , "properties": 4 | { "links" : 5 | { "type" : "array" 6 | , "items" : 7 | { "type" : "object" 8 | , "oneOf" : 9 | [ { "$ref" : "self_link_schema.json"} 10 | , { "$ref" : "up_link_schema.json" } 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/max_length.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/length' 2 | 3 | module JSON 4 | class Schema 5 | class MaxLengthAttribute < LengthLimitAttribute 6 | def self.limit_name 7 | 'maxLength' 8 | end 9 | 10 | def self.error_message(schema) 11 | "was not of a maximum string length of #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/min_items.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/items' 2 | 3 | module JSON 4 | class Schema 5 | class MinItemsAttribute < ItemsLimitAttribute 6 | def self.limit_name 7 | 'minItems' 8 | end 9 | 10 | def self.error_message(schema) 11 | "did not contain a minimum number of items #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/min_length.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/length' 2 | 3 | module JSON 4 | class Schema 5 | class MinLengthAttribute < LengthLimitAttribute 6 | def self.limit_name 7 | 'minLength' 8 | end 9 | 10 | def self.error_message(schema) 11 | "was not of a minimum string length of #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/max_properties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/properties' 2 | 3 | module JSON 4 | class Schema 5 | class MaxPropertiesAttribute < PropertiesLimitAttribute 6 | def self.limit_name 7 | 'maxProperties' 8 | end 9 | 10 | def self.error_message(schema) 11 | "had more properties than the allowed #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/min_properties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limits/properties' 2 | 3 | module JSON 4 | class Schema 5 | class MinPropertiesAttribute < PropertiesLimitAttribute 6 | def self.limit_name 7 | 'minProperties' 8 | end 9 | 10 | def self.error_message(schema) 11 | "did not contain a minimum number of properties #{limit(schema)}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/schemas/any_of_ref_schema.json: -------------------------------------------------------------------------------- 1 | { "$schema" : "http://json-schema.org/draft-04/schema#" 2 | , "type" : "object" 3 | , "properties" : 4 | { "names" : 5 | { "type" : "array" 6 | , "items" : 7 | { "anyOf" : 8 | [ { "$ref" : "any_of_ref_john_schema.json" } 9 | , { "$ref" : "any_of_ref_jane_schema.json" } 10 | , { "$ref" : "any_of_ref_jimmy_schema.json" } 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/properties_v4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/properties' 2 | 3 | module JSON 4 | class Schema 5 | class PropertiesV4Attribute < PropertiesAttribute 6 | # draft4 relies on its own RequiredAttribute validation at a higher level, rather than 7 | # as an attribute of individual properties. 8 | def self.required?(schema, options) 9 | options[:strict] == true 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/disallow.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class DisallowAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless type = validator.attributes['type'] 8 | type.validate(current_schema, data, fragments, processor, validator, options.merge(:disallow => true)) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/min_items_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class MinItemsTest < Minitest::Test 4 | def test_minitems_nils 5 | schema = { 6 | "type" => "array", 7 | "minItems" => 1, 8 | "items" => { "type" => "object" } 9 | } 10 | 11 | errors = JSON::Validator.fully_validate(schema, [nil]) 12 | assert_equal(errors.length, 1) 13 | assert(errors[0] !~ /minimum/) 14 | assert(errors[0] =~ /null/) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limits/numeric.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/limit' 2 | 3 | module JSON 4 | class Schema 5 | class NumericLimitAttribute < LimitAttribute 6 | def self.acceptable_type 7 | Numeric 8 | end 9 | 10 | def self.error_message(schema) 11 | exclusivity = exclusive?(schema) ? 'exclusively' : 'inclusively' 12 | format("did not have a %s value of %s, %s", limit_name, limit(schema), exclusivity) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/format.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class FormatAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data_valid_for_type?(data, current_schema.schema['type']) 8 | format = current_schema.schema['format'].to_s 9 | validator = validator.formats[format] 10 | validator.validate(current_schema, data, fragments, processor, validator, options) unless validator.nil? 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/uniqueitems.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class UniqueItemsAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Array) 8 | 9 | if data.clone.uniq! 10 | message = "The property '#{build_fragment(fragments)}' contained duplicated array values" 11 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/json-schema/util/array_set.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | # This is a hack that I don't want to ever use anywhere else or repeat EVER, but we need enums to be 4 | # an Array to pass schema validation. But we also want fast lookup! 5 | 6 | class ArraySet < Array 7 | def include?(obj) 8 | if !defined? @values 9 | @values = Set.new 10 | self.each { |x| @values << convert_to_float_if_numeric(x) } 11 | end 12 | @values.include?(convert_to_float_if_numeric(obj)) 13 | end 14 | 15 | private 16 | 17 | def convert_to_float_if_numeric(value) 18 | value.is_a?(Numeric) ? value.to_f : value 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: "ruby" 2 | 3 | rvm: 4 | - "1.9" 5 | - "2.0" 6 | - "2.1" 7 | - "2.2" 8 | - "2.3" 9 | - "2.4" 10 | - "2.5" 11 | - "jruby-1.7" 12 | - "jruby-9.1" 13 | 14 | sudo: false 15 | 16 | install: 17 | - bundle install --retry=3 18 | 19 | matrix: 20 | include: 21 | - rvm: "1.9" 22 | gemfile: "gemfiles/Gemfile.ruby_19.x" 23 | - rvm: "jruby-1.7" 24 | gemfile: "gemfiles/Gemfile.ruby_19.x" 25 | - rvm: "2.5" 26 | gemfile: "gemfiles/Gemfile.multi_json.x" 27 | - rvm: "2.5" 28 | gemfile: "gemfiles/Gemfile.yajl-ruby.x" 29 | - rvm: "2.5" 30 | gemfile: "gemfiles/Gemfile.uuidtools.x" 31 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/date_time_v4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | 3 | module JSON 4 | class Schema 5 | class DateTimeV4Format < FormatAttribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(String) 8 | DateTime.rfc3339(data) 9 | rescue ArgumentError 10 | error_message = "The property '#{build_fragment(fragments)}' must be a valid RFC3339 date/time string" 11 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/relative_definition_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class RelativeDefinitionTest < Minitest::Test 4 | 5 | def test_definition_schema 6 | assert_valid schema_fixture_path('definition_schema.json'), {"a" => 5} 7 | end 8 | 9 | def test_definition_schema_with_special_characters 10 | assert_valid schema_fixture_path('definition_schema_with_special_characters.json'), {"a" => 5} 11 | end 12 | 13 | def test_relative_definition 14 | schema = schema_fixture_path('relative_definition_schema.json') 15 | assert_valid schema, {"a" => 5} 16 | refute_valid schema, {"a" => "foo"} 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/uri.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | require 'json-schema/errors/uri_error' 3 | 4 | module JSON 5 | class Schema 6 | class UriFormat < FormatAttribute 7 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 8 | return unless data.is_a?(String) 9 | error_message = "The property '#{build_fragment(fragments)}' must be a valid URI" 10 | begin 11 | JSON::Util::URI.parse(data) 12 | rescue JSON::Schema::UriError 13 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/list_option_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class ListOptionTest < Minitest::Test 4 | def test_list_option_reusing_schemas 5 | schema_hash = { 6 | "$schema" => "http://json-schema.org/draft-04/schema#", 7 | "type" => "object", 8 | "properties" => { "a" => { "type" => "integer" } } 9 | } 10 | 11 | uri = Addressable::URI.parse('http://example.com/item') 12 | schema = JSON::Schema.new(schema_hash, uri) 13 | JSON::Validator.add_schema(schema) 14 | 15 | data = {"a" => 1} 16 | assert_valid uri.to_s, data, clear_cache: false 17 | 18 | data = [{"a" => 1}] 19 | assert_valid uri.to_s, data, :list => true 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/schemas/extends_and_patternProperties_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "extends": {"$ref":"inner_schema.json#"}, 4 | "patternProperties": { 5 | "outerA": { 6 | "description": "blah", 7 | "additionalProperties": false, 8 | "properties": { 9 | "outerA1": { 10 | "type":"boolean" 11 | } 12 | } 13 | }, 14 | "outerB": { 15 | "type": "array", 16 | "minItems": 1, 17 | "maxItems": 50, 18 | "items": { 19 | "extends": {"$ref":"inner_schema.json#"}, 20 | "additionalProperties": false 21 | } 22 | }, 23 | "outerC": { 24 | "description": "blah", 25 | "type":"boolean" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/type_attribute_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class TypeAttributeTest < Minitest::Test 4 | def test_type_of_data 5 | assert_equal(type_of_data(String.new), 'string') 6 | assert_equal(type_of_data(Numeric.new), 'number') 7 | assert_equal(type_of_data(1), 'integer') 8 | assert_equal(type_of_data(true), 'boolean') 9 | assert_equal(type_of_data(false), 'boolean') 10 | assert_equal(type_of_data(Hash.new), 'object') 11 | assert_equal(type_of_data(nil), 'null') 12 | assert_equal(type_of_data(Object.new), 'any') 13 | end 14 | 15 | private 16 | 17 | def type_of_data(data) 18 | JSON::Schema::TypeAttribute.type_of_data(data) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/pattern.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class PatternAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(String) 8 | 9 | pattern = current_schema.schema['pattern'] 10 | regexp = Regexp.new(pattern) 11 | unless regexp.match(data) 12 | message = "The property '#{build_fragment(fragments)}' value #{data.inspect} did not match the regex '#{pattern}'" 13 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/schemas/address_microformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "An Address following the convention of http://microformats.org/wiki/hcard", 3 | "type": "object", 4 | "properties": { 5 | "post-office-box": { "type": "string" }, 6 | "extended-address": { "type": "string" }, 7 | "street-address": { "type": "string" }, 8 | "locality":{ "type": "string" }, 9 | "region": { "type": "string" }, 10 | "postal-code": { "type": "string" }, 11 | "country-name": { "type": "string"} 12 | }, 13 | "required": ["locality", "region", "country-name"], 14 | "dependencies": { 15 | "post-office-box": "street-address", 16 | "extended-address": "street-address" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/schemas/extends_and_additionalProperties_false_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "extends": {"$ref":"inner_schema.json#"}, 4 | "properties": { 5 | "outerA": { 6 | "description": "blah", 7 | "additionalProperties": false, 8 | "properties": { 9 | "outerA1": { 10 | "type":"boolean" 11 | } 12 | } 13 | }, 14 | "outerB": { 15 | "type": "array", 16 | "minItems": 1, 17 | "maxItems": 50, 18 | "items": { 19 | "extends": {"$ref":"inner_schema.json#"}, 20 | "additionalProperties": false 21 | } 22 | }, 23 | "outerC": { 24 | "description": "blah", 25 | "type":"boolean" 26 | } 27 | }, 28 | "additionalProperties": false 29 | } 30 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/maxdecimal.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class MaxDecimalAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Numeric) 8 | 9 | max_decimal_places = current_schema.schema['maxDecimal'] 10 | s = data.to_s.split(".")[1] 11 | if s && s.length > max_decimal_places 12 | message = "The property '#{build_fragment(fragments)}' had more decimal places than the allowed #{max_decimal_places}" 13 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/custom.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | require 'json-schema/errors/custom_format_error' 3 | 4 | module JSON 5 | class Schema 6 | class CustomFormat < FormatAttribute 7 | def initialize(validation_proc) 8 | @validation_proc = validation_proc 9 | end 10 | 11 | def validate(current_schema, data, fragments, processor, validator, options = {}) 12 | begin 13 | @validation_proc.call data 14 | rescue JSON::Schema::CustomFormatError => e 15 | message = "The property '#{self.class.build_fragment(fragments)}' #{e.message}" 16 | self.class.validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/divisibleby.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class DivisibleByAttribute < Attribute 6 | def self.keyword 7 | 'divisibleBy' 8 | end 9 | 10 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 11 | return unless data.is_a?(Numeric) 12 | 13 | factor = current_schema.schema[keyword] 14 | 15 | if factor == 0 || factor == 0.0 || (BigDecimal(data.to_s) % BigDecimal(factor.to_s)).to_f != 0 16 | message = "The property '#{build_fragment(fragments)}' was not divisible by #{factor}" 17 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/json-schema.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | if Gem::Specification::find_all_by_name('multi_json').any? 4 | require 'multi_json' 5 | 6 | # Force MultiJson to load an engine before we define the JSON constant here; otherwise, 7 | # it looks for things that are under the JSON namespace that aren't there (since we have defined it here) 8 | MultiJson.respond_to?(:adapter) ? MultiJson.adapter : MultiJson.engine 9 | end 10 | 11 | require 'json-schema/util/array_set' 12 | require 'json-schema/util/uri' 13 | require 'json-schema/schema' 14 | require 'json-schema/schema/reader' 15 | require 'json-schema/validator' 16 | 17 | Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/**/*.rb")].each {|file| require file } 18 | Dir[File.join(File.dirname(__FILE__), "json-schema/validators/*.rb")].sort!.each {|file| require file } 19 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/patternproperties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class PatternPropertiesAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Hash) 8 | 9 | current_schema.schema['patternProperties'].each do |property, property_schema| 10 | regexp = Regexp.new(property) 11 | 12 | # Check each key in the data hash to see if it matches the regex 13 | data.each do |key, value| 14 | next unless regexp.match(key) 15 | schema = JSON::Schema.new(property_schema, current_schema.uri, validator) 16 | schema.validate(data[key], fragments + [key], processor, options) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/enum.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class EnumAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | enum = current_schema.schema['enum'] 8 | return if enum.include?(data) 9 | 10 | values = enum.map { |val| 11 | case val 12 | when nil then 'null' 13 | when Array then 'array' 14 | when Hash then 'object' 15 | else val.to_s 16 | end 17 | }.join(', ') 18 | 19 | message = "The property '#{build_fragment(fragments)}' value #{data.inspect} did not match one of the following values: #{values}" 20 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | The Ruby JSON Schema library is meant to be a community effort, and as such, there are no strict guidelines for contributing. 2 | 3 | All individuals that have a pull request merged will receive collaborator access to the repository. Due to the restrictions on RubyGems authentication, permissions to release a gem must be requested along with the email desired to be associated with the release credentials. 4 | 5 | Accepting changes to the JSON Schema library shall be made through the use of pull requests on GitHub. A pull request must receive at least two (2) "+1" comments from current contributors, and include a relevant changelog entry, before being accepted and merged. If a breaking issue and fix exists, please feel free to contact the project maintainer at hoxworth@gmail.com or @hoxworth for faster resolution. 6 | 7 | Releases follow semantic versioning and may be made at a maintainer's discretion. 8 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/items.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class ItemsAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Array) 8 | 9 | items = current_schema.schema['items'] 10 | case items 11 | when Hash 12 | schema = JSON::Schema.new(items, current_schema.uri, validator) 13 | data.each_with_index do |item, i| 14 | schema.validate(item, fragments + [i.to_s], processor, options) 15 | end 16 | 17 | when Array 18 | items.each_with_index do |item_schema, i| 19 | break if i >= data.length 20 | schema = JSON::Schema.new(item_schema, current_schema.uri, validator) 21 | schema.validate(data[i], fragments + [i.to_s], processor, options) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/date.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | 3 | module JSON 4 | class Schema 5 | class DateFormat < FormatAttribute 6 | REGEXP = /\A\d{4}-\d{2}-\d{2}\z/ 7 | 8 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 9 | if data.is_a?(String) 10 | error_message = "The property '#{build_fragment(fragments)}' must be a date in the format of YYYY-MM-DD" 11 | if REGEXP.match(data) 12 | begin 13 | Date.parse(data) 14 | rescue ArgumentError => e 15 | raise e unless e.message == 'invalid date' 16 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 17 | end 18 | else 19 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTORS 2 | ------------ 3 | 4 | * Kenny Hoxworth - @hoxworth 5 | * Jenny Duckett - @jennyd 6 | * Kevin Glowacz - @kjg 7 | * Seb Bacon - @sebbacon 8 | * Jonathan Chrisp - @jonathanchrisp 9 | * James McKinney - @jpmckinney 10 | * Kylo Ginsberg - @kylog 11 | * Alex Soto - @apsoto 12 | * Roger Leite - @rogerleite 13 | * Myron Marston - @myronmarston 14 | * Jesse Stuart - @jvatic 15 | * Brian Hoffman - @lcdhoffman 16 | * Simon Waddington - @goodsimon 17 | * Chris Baynes - @chris-baynes 18 | * David Barri - @japgolly 19 | * Tyler Hunt - @tylerhunt 20 | * @vapir 21 | * Tom May - @tommay 22 | * Chris Johnson - @kindkid 23 | * David Kellum - @dekellum 24 | * Miguel Herranz - @IPGlider 25 | * Nick Recobra - @oruen 26 | * Vasily Fedoseyev - @Vasfed 27 | * Jari Bakken - @jarib 28 | * Kyle Hargraves - @pd 29 | * Jamie Cobbett - @jamiecobbett 30 | * Iain Beeston - @iainbeeston 31 | * Matt Palmer - @mpalmer 32 | * Ben Kirzhner - @benkirzhner 33 | * RST-J - @RST-J 34 | * Christian Treppo - @treppo 35 | * Benjamin Falk - @benfalk 36 | -------------------------------------------------------------------------------- /json-schema.gemspec: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | version_yaml = YAML.load(File.open(File.expand_path('../VERSION.yml', __FILE__)).read) 4 | version = "#{version_yaml['major']}.#{version_yaml['minor']}.#{version_yaml['patch']}" 5 | gem_name = "json-schema" 6 | 7 | Gem::Specification.new do |s| 8 | s.name = gem_name 9 | s.version = version 10 | s.authors = ["Kenny Hoxworth"] 11 | s.email = "hoxworth@gmail.com" 12 | s.homepage = "http://github.com/ruby-json-schema/json-schema/tree/master" 13 | s.summary = "Ruby JSON Schema Validator" 14 | s.files = Dir[ "lib/**/*", "resources/*.json" ] 15 | s.require_path = "lib" 16 | s.extra_rdoc_files = ["README.md","LICENSE.md"] 17 | s.required_ruby_version = ">= 1.9" 18 | s.license = "MIT" 19 | s.required_rubygems_version = ">= 1.8" 20 | 21 | s.add_development_dependency "rake" 22 | s.add_development_dependency "minitest", '~> 5.0' 23 | s.add_development_dependency "webmock" 24 | s.add_development_dependency "bundler" 25 | 26 | s.add_runtime_dependency "addressable", '>= 2.4' 27 | end 28 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/type_v4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class TypeV4Attribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | union = true 8 | types = current_schema.schema['type'] 9 | if !types.is_a?(Array) 10 | types = [types] 11 | union = false 12 | end 13 | 14 | return if types.any? { |type| data_valid_for_type?(data, type) } 15 | 16 | types = types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ') 17 | message = format( 18 | "The property '%s' of type %s did not match %s: %s", 19 | build_fragment(fragments), 20 | type_of_data(data), 21 | union ? 'one or more of the following types' : 'the following type', 22 | types 23 | ) 24 | 25 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/properties_optional.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class PropertiesOptionalAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Hash) 8 | 9 | schema = current_schema.schema 10 | schema['properties'].each do |property, property_schema| 11 | property = property.to_s 12 | 13 | if !property_schema['optional'] && !data.key?(property) 14 | message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'" 15 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 16 | end 17 | 18 | if data.has_key?(property) 19 | expected_schema = JSON::Schema.new(property_schema, current_schema.uri, validator) 20 | expected_schema.validate(data[property], fragments + [property], processor, options) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011, Lookingglass Cyber Solutions 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/time.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | 3 | module JSON 4 | class Schema 5 | class TimeFormat < FormatAttribute 6 | REGEXP = /\A(\d{2}):(\d{2}):(\d{2})\z/ 7 | 8 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 9 | if data.is_a?(String) 10 | error_message = "The property '#{build_fragment(fragments)}' must be a time in the format of hh:mm:ss" 11 | if (m = REGEXP.match(data)) 12 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[1].to_i > 23 13 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[2].to_i > 59 14 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[3].to_i > 59 15 | else 16 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/required.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class RequiredAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Hash) 8 | 9 | schema = current_schema.schema 10 | defined_properties = schema['properties'] 11 | 12 | schema['required'].each do |property, property_schema| 13 | next if data.has_key?(property.to_s) 14 | prop_defaults = options[:insert_defaults] && 15 | defined_properties && 16 | defined_properties[property] && 17 | !defined_properties[property]["default"].nil? && 18 | !defined_properties[property]["readonly"] 19 | 20 | if !prop_defaults 21 | message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'" 22 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/additionalitems.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class AdditionalItemsAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Array) 8 | 9 | schema = current_schema.schema 10 | return unless schema['items'].is_a?(Array) 11 | 12 | case schema['additionalItems'] 13 | when false 14 | if schema['items'].length < data.length 15 | message = "The property '#{build_fragment(fragments)}' contains additional array elements outside of the schema when none are allowed" 16 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 17 | end 18 | when Hash 19 | additional_items_schema = JSON::Schema.new(schema['additionalItems'], current_schema.uri, validator) 20 | data.each_with_index do |item, i| 21 | next if i < schema['items'].length 22 | additional_items_schema.validate(item, fragments + [i.to_s], processor, options) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/ip.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | require 'ipaddr' 3 | require 'socket' 4 | 5 | module JSON 6 | class Schema 7 | class IPFormat < FormatAttribute 8 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 9 | return unless data.is_a?(String) 10 | 11 | begin 12 | ip = IPAddr.new(data) 13 | rescue ArgumentError => e 14 | raise e unless e.message == 'invalid address' 15 | end 16 | 17 | family = ip_version == 6 ? Socket::AF_INET6 : Socket::AF_INET 18 | unless ip && ip.family == family 19 | error_message = "The property '#{build_fragment(fragments)}' must be a valid IPv#{ip_version} address" 20 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 21 | end 22 | end 23 | 24 | def self.ip_version 25 | raise NotImplementedError 26 | end 27 | end 28 | 29 | class IP4Format < IPFormat 30 | def self.ip_version 31 | 4 32 | end 33 | end 34 | 35 | class IP6Format < IPFormat 36 | def self.ip_version 37 | 6 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/not.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class NotAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | schema = JSON::Schema.new(current_schema.schema['not'],current_schema.uri,validator) 8 | failed = true 9 | errors_copy = processor.validation_errors.clone 10 | 11 | begin 12 | schema.validate(data,fragments,processor,options) 13 | # If we're recording errors, we don't throw an exception. Instead, check the errors array length 14 | if options[:record_errors] && errors_copy.length != processor.validation_errors.length 15 | processor.validation_errors.replace(errors_copy) 16 | else 17 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} matched the disallowed schema" 18 | failed = false 19 | end 20 | rescue ValidationError 21 | # Yay, we failed validation. 22 | end 23 | 24 | unless failed 25 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/all_of_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class AllOfRefSchemaTest < Minitest::Test 4 | def schema 5 | schema_fixture_path('all_of_ref_schema.json') 6 | end 7 | 8 | def data 9 | data_fixture_path('all_of_ref_data.json') 10 | end 11 | 12 | def test_all_of_ref_schema_fails 13 | refute_valid schema, data 14 | end 15 | 16 | def test_all_of_ref_schema_succeeds 17 | assert_valid schema, %({"name": 42}) 18 | end 19 | 20 | def test_all_of_ref_subschema_errors 21 | errors = JSON::Validator.fully_validate(schema, data, :errors_as_objects => true) 22 | nested_errors = errors[0][:errors] 23 | assert_equal([:allof_0], nested_errors.keys, 'should have nested errors for each allOf subschema') 24 | assert_match(/the property '#\/name' of type string did not match the following type: integer/i, nested_errors[:allof_0][0][:message]) 25 | end 26 | 27 | def test_all_of_ref_message 28 | errors = JSON::Validator.fully_validate(schema, data) 29 | expected_message = """The property '#/' of type object did not match all of the required schemas. The schema specific errors were: 30 | 31 | - allOf #0: 32 | - The property '#/name' of type string did not match the following type: integer""" 33 | assert_equal(expected_message, errors[0]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/json-schema/schema/validator.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class Validator 4 | attr_accessor :attributes, :formats, :uri, :names 5 | attr_reader :default_formats 6 | 7 | def initialize() 8 | @attributes = {} 9 | @formats = {} 10 | @default_formats = {} 11 | @uri = nil 12 | @names = [] 13 | @metaschema_name = '' 14 | end 15 | 16 | def extend_schema_definition(schema_uri) 17 | warn "[DEPRECATION NOTICE] The preferred way to extend a Validator is by subclassing, rather than #extend_schema_definition. This method will be removed in version >= 3." 18 | validator = JSON::Validator.validator_for_uri(schema_uri) 19 | @attributes.merge!(validator.attributes) 20 | end 21 | 22 | def validate(current_schema, data, fragments, processor, options = {}) 23 | current_schema.schema.each do |attr_name,attribute| 24 | if @attributes.has_key?(attr_name.to_s) 25 | @attributes[attr_name.to_s].validate(current_schema, data, fragments, processor, self, options) 26 | end 27 | end 28 | data 29 | end 30 | 31 | def metaschema 32 | resources = File.expand_path('../../../../resources', __FILE__) 33 | File.join(resources, @metaschema_name) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/load_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class LoadRefSchemaTest < Minitest::Test 4 | def load_other_schema 5 | JSON::Validator.add_schema(JSON::Schema.new( 6 | { 7 | '$schema' => 'http://json-schema.org/draft-04/schema#', 8 | 'type' => 'object', 9 | 'properties' => { 10 | "title" => { 11 | "type" => "string" 12 | } 13 | } 14 | }, 15 | Addressable::URI.parse("http://example.com/schema#") 16 | )) 17 | end 18 | 19 | def test_cached_schema 20 | schema_url = "http://example.com/schema#" 21 | schema = { "$ref" => schema_url } 22 | data = {} 23 | load_other_schema 24 | _validator = JSON::Validator.new(schema, data) 25 | 26 | assert JSON::Validator.schema_loaded?(schema_url) 27 | end 28 | 29 | def test_cached_schema_with_fragment 30 | schema_url = "http://example.com/schema#" 31 | schema = { "$ref" => "#{schema_url}/properties/title" } 32 | data = {} 33 | load_other_schema 34 | _validator = JSON::Validator.new(schema, data) 35 | 36 | assert JSON::Validator.schema_loaded?(schema_url) 37 | end 38 | 39 | def test_metaschema 40 | schema = { "$ref" => "http://json-schema.org/draft-04/schema#" } 41 | data = {} 42 | 43 | assert_valid schema, data 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/caching_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class CachingTestTest < Minitest::Test 4 | def setup 5 | @schema = Tempfile.new(['schema', '.json']) 6 | end 7 | 8 | def teardown 9 | @schema.close 10 | @schema.unlink 11 | 12 | JSON::Validator.clear_cache 13 | end 14 | 15 | def test_caching 16 | set_schema('type' => 'string') 17 | assert_valid(schema_path, 'foo', :clear_cache => false) 18 | 19 | set_schema('type' => 'number') 20 | refute_valid(schema_path, 123) 21 | end 22 | 23 | def test_clear_cache 24 | set_schema('type' => 'string') 25 | assert_valid(schema_path, 'foo', :clear_cache => true) 26 | 27 | set_schema('type' => 'number') 28 | assert_valid(schema_path, 123) 29 | end 30 | 31 | def test_cache_schemas 32 | suppress_warnings do 33 | JSON::Validator.cache_schemas = false 34 | end 35 | 36 | set_schema('type' => 'string') 37 | assert_valid(schema_path, 'foo', :clear_cache => false) 38 | 39 | set_schema('type' => 'number') 40 | assert_valid(schema_path, 123) 41 | ensure 42 | suppress_warnings do 43 | JSON::Validator.cache_schemas = true 44 | end 45 | end 46 | 47 | private 48 | 49 | def schema_path 50 | @schema.path 51 | end 52 | 53 | def set_schema(schema_definition) 54 | @schema.write(schema_definition.to_json) 55 | @schema.rewind 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/stringify_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class StringifyTest < Minitest::Test 4 | def test_stringify_on_hash 5 | hash = { 6 | :a => 'foo', 7 | 'b' => :bar 8 | } 9 | assert_equal({'a' => 'foo', 'b' => 'bar'}, JSON::Schema.stringify(hash), 'symbol keys should be converted to strings') 10 | end 11 | 12 | def test_stringify_on_array 13 | array = [ 14 | :a, 15 | 'b' 16 | ] 17 | assert_equal(['a', 'b'], JSON::Schema.stringify(array), 'symbols in an array should be converted to strings') 18 | end 19 | 20 | def test_stringify_on_hash_of_arrays 21 | hash = { 22 | :a => [:foo], 23 | 'b' => :bar 24 | } 25 | assert_equal({'a' => ['foo'], 'b' => 'bar'}, JSON::Schema.stringify(hash), 'symbols in a nested array should be converted to strings') 26 | end 27 | 28 | def test_stringify_on_array_of_hashes 29 | array = [ 30 | :a, 31 | { 32 | :b => :bar 33 | } 34 | ] 35 | assert_equal(['a', {'b' => 'bar'}], JSON::Schema.stringify(array), 'symbols keys in a nested hash should be converted to strings') 36 | end 37 | 38 | def test_stringify_on_hash_of_hashes 39 | hash = { 40 | :a => { 41 | :b => { 42 | :foo => :bar 43 | } 44 | } 45 | } 46 | assert_equal({'a' => {'b' => {'foo' => 'bar'} } }, JSON::Schema.stringify(hash), 'symbols in a nested hash of hashes should be converted to strings') 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/limit.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class LimitAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | schema = current_schema.schema 8 | return unless data.is_a?(acceptable_type) && invalid?(schema, value(data)) 9 | 10 | property = build_fragment(fragments) 11 | description = error_message(schema) 12 | message = format("The property '%s' %s", property, description) 13 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 14 | end 15 | 16 | def self.invalid?(schema, data) 17 | exclusive = exclusive?(schema) 18 | limit = limit(schema) 19 | 20 | if limit_name.start_with?('max') 21 | exclusive ? data >= limit : data > limit 22 | else 23 | exclusive ? data <= limit : data < limit 24 | end 25 | end 26 | 27 | def self.limit(schema) 28 | schema[limit_name] 29 | end 30 | 31 | def self.exclusive?(schema) 32 | false 33 | end 34 | 35 | def self.value(data) 36 | data 37 | end 38 | 39 | def self.acceptable_type 40 | raise NotImplementedError 41 | end 42 | 43 | def self.error_message(schema) 44 | raise NotImplementedError 45 | end 46 | 47 | def self.limit_name 48 | raise NotImplementedError 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/ruby_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class RubySchemaTest < Minitest::Test 4 | def test_string_keys 5 | schema = { 6 | "type" => 'object', 7 | "required" => ["a"], 8 | "properties" => { 9 | "a" => {"type" => "integer", "default" => 42}, 10 | "b" => {"type" => "integer"} 11 | } 12 | } 13 | 14 | assert_valid schema, { "a" => 5 } 15 | end 16 | 17 | def test_symbol_keys 18 | schema = { 19 | :type => 'object', 20 | :required => ["a"], 21 | :properties => { 22 | :a => {:type => "integer", :default => 42}, 23 | :b => {:type => "integer"} 24 | } 25 | } 26 | 27 | assert_valid schema, { :a => 5 } 28 | end 29 | 30 | def test_symbol_keys_in_hash_within_array 31 | schema = { 32 | :type => 'object', 33 | :properties => { 34 | :a => { 35 | :type => "array", 36 | :items => [ 37 | { 38 | :properties => { 39 | :b => { 40 | :type => "integer" 41 | } 42 | } 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | 49 | data = { 50 | :a => [ 51 | { 52 | :b => 1 53 | } 54 | ] 55 | } 56 | 57 | assert_valid schema, data, :validate_schema => true 58 | end 59 | 60 | def test_schema_of_unrecognized_type 61 | assert_raises JSON::Schema::SchemaParseError do 62 | JSON::Validator.validate(Object.new, {}) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/extends_nested_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class ExtendsNestedTest < Minitest::Test 4 | ADDITIONAL_PROPERTIES = ['extends_and_additionalProperties_false_schema.json'] 5 | PATTERN_PROPERTIES = ['extends_and_patternProperties_schema.json'] 6 | 7 | ALL_SCHEMAS = ADDITIONAL_PROPERTIES + PATTERN_PROPERTIES 8 | 9 | def test_valid_outer 10 | ALL_SCHEMAS.each do |file| 11 | path = schema_fixture_path(file) 12 | assert_valid path, { "outerC" => true }, {}, "Outer defn is broken, maybe the outer extends overrode it" 13 | end 14 | end 15 | 16 | def test_valid_outer_extended 17 | ALL_SCHEMAS.each do |file| 18 | path = schema_fixture_path(file) 19 | assert_valid path, { "innerA" => true }, {}, "Extends at the root level isn't working" 20 | end 21 | end 22 | 23 | def test_valid_inner 24 | ALL_SCHEMAS.each do |file| 25 | path = schema_fixture_path(file) 26 | assert_valid path, { "outerB" => [{ "innerA" => true }] }, {}, "Extends isn't working in the array element defn" 27 | end 28 | end 29 | 30 | def test_invalid_inner 31 | ALL_SCHEMAS.each do |file| 32 | path = schema_fixture_path(file) 33 | refute_valid path, { "outerB" => [{ "whaaaaat" => true }] }, {}, "Array element defn allowing anything when it should only allow what's in inner.schema" 34 | end 35 | end 36 | 37 | def test_invalid_outer 38 | path = schema_fixture_path(ADDITIONAL_PROPERTIES) 39 | refute_valid path, { "whaaaaat" => true }, {}, "Outer defn allowing anything when it shouldn't" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/test_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require 'simplecov' 3 | SimpleCov.start do 4 | add_filter '/test/' 5 | end 6 | end 7 | 8 | require 'minitest/autorun' 9 | require 'webmock/minitest' 10 | 11 | $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) 12 | require 'json-schema' 13 | 14 | Dir[File.join(File.expand_path('../', __FILE__), '*.rb')].each do |support_file| 15 | require support_file unless support_file == __FILE__ 16 | end 17 | 18 | class Minitest::Test 19 | def suppress_warnings 20 | old_verbose = $VERBOSE 21 | $VERBOSE = nil 22 | begin 23 | yield 24 | ensure 25 | $VERBOSE = old_verbose 26 | end 27 | end 28 | 29 | def schema_fixture_path(filename) 30 | File.join(File.dirname(__FILE__), '../schemas', filename) 31 | end 32 | 33 | def data_fixture_path(filename) 34 | File.join(File.dirname(__FILE__), '../data', filename) 35 | end 36 | 37 | def assert_valid(schema, data, options = {}, msg = "#{data.inspect} should be valid for schema:\n#{schema.inspect}") 38 | errors = validation_errors(schema, data, options) 39 | assert_equal([], errors, msg) 40 | end 41 | 42 | def refute_valid(schema, data, options = {}, msg = "#{data.inspect} should be invalid for schema:\n#{schema.inspect}") 43 | errors = validation_errors(schema, data, options) 44 | refute_equal([], errors, msg) 45 | end 46 | 47 | def validation_errors(schema, data, options) 48 | options = { :clear_cache => true, :validate_schema => true }.merge(options) 49 | JSON::Validator.fully_validate(schema, data, options) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/bad_schema_ref_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | require 'socket' 3 | 4 | 5 | class BadSchemaRefTest < Minitest::Test 6 | def setup 7 | WebMock.allow_net_connect! 8 | end 9 | 10 | def teardown 11 | WebMock.disable_net_connect! 12 | end 13 | 14 | def test_bad_uri_ref 15 | schema = { 16 | "$schema" => "http://json-schema.org/draft-04/schema#", 17 | "type" => "array", 18 | "items" => { "$ref" => "../google.json"} 19 | } 20 | 21 | data = [1,2,3] 22 | error = assert_raises(JSON::Schema::ReadFailed) do 23 | JSON::Validator.validate(schema,data) 24 | end 25 | 26 | expanded_path = File.expand_path("../../google.json", __FILE__) 27 | 28 | assert_equal(:file, error.type) 29 | assert_equal(expanded_path, error.location) 30 | assert_equal("Read of file at #{expanded_path} failed", error.message) 31 | end 32 | 33 | def test_bad_host_ref 34 | schema = { 35 | "$schema" => "http://json-schema.org/draft-04/schema#", 36 | "type" => "array", 37 | "items" => { "$ref" => "http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema"} 38 | } 39 | 40 | data = [1,2,3] 41 | error = assert_raises(JSON::Schema::ReadFailed) do 42 | JSON::Validator.validate(schema,data) 43 | end 44 | 45 | assert_equal(:uri, error.type) 46 | assert_equal("http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema", error.location) 47 | assert_equal("Read of URI at http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema failed", error.message) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/dependencies.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class DependenciesAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Hash) 8 | 9 | current_schema.schema['dependencies'].each do |property, dependency_value| 10 | next unless data.has_key?(property.to_s) 11 | next unless accept_value?(dependency_value) 12 | 13 | case dependency_value 14 | when String 15 | validate_dependency(current_schema, data, property, dependency_value, fragments, processor, self, options) 16 | when Array 17 | dependency_value.each do |value| 18 | validate_dependency(current_schema, data, property, value, fragments, processor, self, options) 19 | end 20 | else 21 | schema = JSON::Schema.new(dependency_value, current_schema.uri, validator) 22 | schema.validate(data, fragments, processor, options) 23 | end 24 | end 25 | end 26 | 27 | def self.validate_dependency(schema, data, property, value, fragments, processor, attribute, options) 28 | return if data.key?(value.to_s) 29 | message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{value}'" 30 | validation_error(processor, message, fragments, schema, attribute, options[:record_errors]) 31 | end 32 | 33 | def self.accept_value?(value) 34 | value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/any_of_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class AnyOfRefSchemaTest < Minitest::Test 4 | def schema 5 | schema_fixture_path('any_of_ref_schema.json') 6 | end 7 | 8 | def test_any_of_ref_schema 9 | assert_valid schema, data_fixture_path('any_of_ref_data.json') 10 | end 11 | 12 | def test_any_of_ref_subschema_errors 13 | data = %({"names": ["jack"]}) 14 | errors = JSON::Validator.fully_validate(schema, data, :errors_as_objects => true) 15 | nested_errors = errors[0][:errors] 16 | assert_equal([:anyof_0, :anyof_1, :anyof_2], nested_errors.keys, 'should have nested errors for each anyOf subschema') 17 | assert_match(/the property '#\/names\/0' value "jack" did not match the regex 'john'/i, nested_errors[:anyof_0][0][:message]) 18 | assert_match(/the property '#\/names\/0' value "jack" did not match the regex 'jane'/i, nested_errors[:anyof_1][0][:message]) 19 | assert_match(/the property '#\/names\/0' value "jack" did not match the regex 'jimmy'/i, nested_errors[:anyof_2][0][:message]) 20 | end 21 | 22 | def test_any_of_ref_message 23 | data = %({"names": ["jack"]}) 24 | errors = JSON::Validator.fully_validate(schema, data) 25 | expected_message = """The property '#/names/0' of type string did not match one or more of the required schemas. The schema specific errors were: 26 | 27 | - anyOf #0: 28 | - The property '#/names/0' value \"jack\" did not match the regex 'john' 29 | - anyOf #1: 30 | - The property '#/names/0' value \"jack\" did not match the regex 'jane' 31 | - anyOf #2: 32 | - The property '#/names/0' value \"jack\" did not match the regex 'jimmy'""" 33 | assert_equal(expected_message, errors[0]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/json-schema/attribute.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/errors/validation_error' 2 | 3 | module JSON 4 | class Schema 5 | class Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | end 8 | 9 | def self.build_fragment(fragments) 10 | "#/#{fragments.join('/')}" 11 | end 12 | 13 | def self.validation_error(processor, message, fragments, current_schema, failed_attribute, record_errors) 14 | error = ValidationError.new(message, fragments, failed_attribute, current_schema) 15 | if record_errors 16 | processor.validation_error(error) 17 | else 18 | raise error 19 | end 20 | end 21 | 22 | def self.validation_errors(validator) 23 | validator.validation_errors 24 | end 25 | 26 | TYPE_CLASS_MAPPINGS = { 27 | "string" => String, 28 | "number" => Numeric, 29 | "integer" => Integer, 30 | "boolean" => [TrueClass, FalseClass], 31 | "object" => Hash, 32 | "array" => Array, 33 | "null" => NilClass, 34 | "any" => Object 35 | } 36 | 37 | def self.data_valid_for_type?(data, type) 38 | valid_classes = TYPE_CLASS_MAPPINGS.fetch(type) { return true } 39 | Array(valid_classes).any? { |c| data.is_a?(c) } 40 | end 41 | 42 | # Lookup Schema type of given class instance 43 | def self.type_of_data(data) 44 | type, _ = TYPE_CLASS_MAPPINGS.map { |k,v| [k,v] }.sort_by { |(_, v)| 45 | -Array(v).map { |klass| klass.ancestors.size }.max 46 | }.find { |(_, v)| 47 | Array(v).any? { |klass| data.kind_of?(klass) } 48 | } 49 | type 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/allof.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class AllOfAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | # Create an hash to hold errors that are generated during validation 8 | errors = Hash.new { |hsh, k| hsh[k] = [] } 9 | valid = true 10 | 11 | current_schema.schema['allOf'].each_with_index do |element, schema_index| 12 | schema = JSON::Schema.new(element,current_schema.uri,validator) 13 | 14 | # We're going to add a little cruft here to try and maintain any validation errors that occur in the allOf 15 | # We'll handle this by keeping an error count before and after validation, extracting those errors and pushing them onto an error array 16 | pre_validation_error_count = validation_errors(processor).count 17 | 18 | begin 19 | schema.validate(data,fragments,processor,options) 20 | rescue ValidationError 21 | valid = false 22 | end 23 | 24 | diff = validation_errors(processor).count - pre_validation_error_count 25 | while diff > 0 26 | diff = diff - 1 27 | errors["allOf ##{schema_index}"].push(validation_errors(processor).pop) 28 | end 29 | end 30 | 31 | if !valid || !errors.empty? 32 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match all of the required schemas" 33 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 34 | validation_errors(processor).last.sub_errors = errors 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/validator_schema_reader_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class ValidatorSchemaReaderTest < Minitest::Test 4 | 5 | class MockReader < JSON::Schema::Reader 6 | def read(location) 7 | return super unless location.to_s == 'http://any.url/at/all' 8 | 9 | schema = { 10 | '$schema' => 'http://json-schema.org/draft-04/schema#', 11 | 'type' => 'string', 12 | 'minLength' => 2 13 | } 14 | 15 | JSON::Schema.new(schema, Addressable::URI.parse(location.to_s)) 16 | end 17 | end 18 | 19 | def setup 20 | @original_reader = JSON::Validator.schema_reader 21 | end 22 | 23 | def teardown 24 | JSON::Validator.schema_reader = @original_reader 25 | end 26 | 27 | def test_default_schema_reader 28 | reader = JSON::Validator.schema_reader 29 | assert reader.accept_uri?(Addressable::URI.parse('http://example.com')) 30 | assert reader.accept_file?(Pathname.new('/etc/passwd')) 31 | end 32 | 33 | def test_set_default_schema_reader 34 | JSON::Validator.schema_reader = MockReader.new 35 | 36 | schema = { '$ref' => 'http://any.url/at/all' } 37 | assert_valid schema, 'abc' 38 | refute_valid schema, 'a' 39 | end 40 | 41 | def test_validate_with_reader 42 | reader = MockReader.new 43 | schema = { '$ref' => 'http://any.url/at/all' } 44 | assert_valid schema, 'abc', :schema_reader => reader 45 | refute_valid schema, 'a', :schema_reader => reader 46 | end 47 | 48 | def test_validate_list_with_reader 49 | reader = MockReader.new 50 | schema = { '$ref' => 'http://any.url/at/all' } 51 | assert_valid schema, ['abc', 'def'], :schema_reader => reader, :list => true 52 | refute_valid schema, ['abc', 'a'], :schema_reader => reader, :list => true 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft1.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | 6 | class Draft1 < Validator 7 | def initialize 8 | super 9 | @attributes = { 10 | "type" => JSON::Schema::TypeAttribute, 11 | "disallow" => JSON::Schema::DisallowAttribute, 12 | "format" => JSON::Schema::FormatAttribute, 13 | "maximum" => JSON::Schema::MaximumInclusiveAttribute, 14 | "minimum" => JSON::Schema::MinimumInclusiveAttribute, 15 | "minItems" => JSON::Schema::MinItemsAttribute, 16 | "maxItems" => JSON::Schema::MaxItemsAttribute, 17 | "minLength" => JSON::Schema::MinLengthAttribute, 18 | "maxLength" => JSON::Schema::MaxLengthAttribute, 19 | "maxDecimal" => JSON::Schema::MaxDecimalAttribute, 20 | "enum" => JSON::Schema::EnumAttribute, 21 | "properties" => JSON::Schema::PropertiesOptionalAttribute, 22 | "pattern" => JSON::Schema::PatternAttribute, 23 | "additionalProperties" => JSON::Schema::AdditionalPropertiesAttribute, 24 | "items" => JSON::Schema::ItemsAttribute, 25 | "extends" => JSON::Schema::ExtendsAttribute 26 | } 27 | @default_formats = { 28 | 'date-time' => DateTimeFormat, 29 | 'date' => DateFormat, 30 | 'time' => TimeFormat, 31 | 'ip-address' => IP4Format, 32 | 'ipv6' => IP6Format, 33 | 'uri' => UriFormat 34 | } 35 | @formats = @default_formats.clone 36 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-01/schema#") 37 | @names = ["draft1"] 38 | @metaschema_name = "draft-01.json" 39 | end 40 | 41 | JSON::Validator.register_validator(self.new) 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/json-schema/errors/validation_error.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class ValidationError < StandardError 4 | INDENT = " " 5 | attr_accessor :fragments, :schema, :failed_attribute, :sub_errors, :message 6 | 7 | def initialize(message, fragments, failed_attribute, schema) 8 | @fragments = fragments.clone 9 | @schema = schema 10 | @sub_errors = {} 11 | @failed_attribute = failed_attribute 12 | @message = message 13 | super(message_with_schema) 14 | end 15 | 16 | def to_string(subschema_level = 0) 17 | if @sub_errors.empty? 18 | subschema_level == 0 ? message_with_schema : message 19 | else 20 | messages = ["#{message}. The schema specific errors were:\n"] 21 | @sub_errors.each do |subschema, errors| 22 | messages.push "- #{subschema}:" 23 | messages.concat Array(errors).map { |e| "#{INDENT}- #{e.to_string(subschema_level + 1)}" } 24 | end 25 | messages.map { |m| (INDENT * subschema_level) + m }.join("\n") 26 | end 27 | end 28 | 29 | def to_hash 30 | base = {:schema => @schema.uri, :fragment => ::JSON::Schema::Attribute.build_fragment(fragments), :message => message_with_schema, :failed_attribute => @failed_attribute.to_s.split(":").last.split("Attribute").first} 31 | if !@sub_errors.empty? 32 | base[:errors] = @sub_errors.inject({}) do |hsh, (subschema, errors)| 33 | subschema_sym = subschema.downcase.gsub(/\W+/, '_').to_sym 34 | hsh[subschema_sym] = Array(errors).map{|e| e.to_hash} 35 | hsh 36 | end 37 | end 38 | base 39 | end 40 | 41 | def message_with_schema 42 | "#{message} in schema #{schema.uri}" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rake' 3 | require 'rake/testtask' 4 | 5 | Bundler::GemHelper.install_tasks 6 | 7 | desc "Updates the json-schema common test suite to the latest version" 8 | task :update_common_tests do 9 | unless File.read(".git/config").include?('submodule "test/test-suite"') 10 | sh "git submodule init" 11 | end 12 | 13 | puts "Updating json-schema common test suite..." 14 | 15 | begin 16 | sh "git submodule update --remote --quiet" 17 | rescue StandardError 18 | STDERR.puts "Failed to update common test suite." 19 | end 20 | end 21 | 22 | desc "Update meta-schemas to the latest version" 23 | task :update_meta_schemas do 24 | puts "Updating meta-schemas..." 25 | 26 | id_mappings = { 27 | 'http://json-schema.org/draft/schema#' => 'https://raw.githubusercontent.com/json-schema-org/json-schema-spec/master/schema.json' 28 | } 29 | 30 | require 'open-uri' 31 | require 'thwait' 32 | 33 | download_threads = Dir['resources/*.json'].map do |path| 34 | schema_id = File.read(path)[/"\$?id"\s*:\s*"(.*?)"/, 1] 35 | schema_uri = id_mappings[schema_id] || schema_id 36 | 37 | Thread.new(schema_uri) do |uri| 38 | Thread.current[:uri] = uri 39 | 40 | begin 41 | metaschema = URI(uri).read 42 | 43 | File.write(path, metaschema) 44 | rescue StandardError 45 | false 46 | end 47 | end 48 | end 49 | 50 | ThreadsWait.all_waits(*download_threads) do |t| 51 | if t.value 52 | puts t[:uri] 53 | else 54 | STDERR.puts "Failed to update meta-schema #{t[:uri]}" 55 | end 56 | end 57 | end 58 | 59 | Rake::TestTask.new do |t| 60 | t.libs << "." 61 | t.warning = true 62 | t.verbose = true 63 | t.test_files = FileList.new('test/*_test.rb') 64 | end 65 | 66 | task update: [:update_common_tests, :update_meta_schemas] 67 | 68 | task :default => :test 69 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/formats/date_time.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attributes/format' 2 | 3 | module JSON 4 | class Schema 5 | class DateTimeFormat < FormatAttribute 6 | REGEXP = /\A\d{4}-\d{2}-\d{2}T(\d{2}):(\d{2}):(\d{2})([\.,]\d+)?(Z|[+-](\d{2})(:?\d{2})?)?\z/ 7 | 8 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 9 | # Timestamp in restricted ISO-8601 YYYY-MM-DDThh:mm:ssZ with optional decimal fraction of the second 10 | if data.is_a?(String) 11 | error_message = "The property '#{build_fragment(fragments)}' must be a date/time in the ISO-8601 format of YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss.ssZ" 12 | if (m = REGEXP.match(data)) 13 | parts = data.split("T") 14 | 15 | begin 16 | Date.parse(parts[0]) 17 | rescue ArgumentError => e 18 | raise e unless e.message == 'invalid date' 19 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 20 | return 21 | end 22 | 23 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m.length < 4 24 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[1].to_i > 23 25 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[2].to_i > 59 26 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[3].to_i > 59 27 | else 28 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft2.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | 6 | class Draft2 < Validator 7 | def initialize 8 | super 9 | @attributes = { 10 | "type" => JSON::Schema::TypeAttribute, 11 | "disallow" => JSON::Schema::DisallowAttribute, 12 | "format" => JSON::Schema::FormatAttribute, 13 | "maximum" => JSON::Schema::MaximumInclusiveAttribute, 14 | "minimum" => JSON::Schema::MinimumInclusiveAttribute, 15 | "minItems" => JSON::Schema::MinItemsAttribute, 16 | "maxItems" => JSON::Schema::MaxItemsAttribute, 17 | "uniqueItems" => JSON::Schema::UniqueItemsAttribute, 18 | "minLength" => JSON::Schema::MinLengthAttribute, 19 | "maxLength" => JSON::Schema::MaxLengthAttribute, 20 | "divisibleBy" => JSON::Schema::DivisibleByAttribute, 21 | "enum" => JSON::Schema::EnumAttribute, 22 | "properties" => JSON::Schema::PropertiesOptionalAttribute, 23 | "pattern" => JSON::Schema::PatternAttribute, 24 | "additionalProperties" => JSON::Schema::AdditionalPropertiesAttribute, 25 | "items" => JSON::Schema::ItemsAttribute, 26 | "extends" => JSON::Schema::ExtendsAttribute 27 | } 28 | @default_formats = { 29 | 'date-time' => DateTimeFormat, 30 | 'date' => DateFormat, 31 | 'time' => TimeFormat, 32 | 'ip-address' => IP4Format, 33 | 'ipv6' => IP6Format, 34 | 'uri' => UriFormat 35 | } 36 | @formats = @default_formats.clone 37 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-02/schema#") 38 | @names = ["draft2"] 39 | @metaschema_name = "draft-02.json" 40 | end 41 | 42 | JSON::Validator.register_validator(self.new) 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/extends.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | require 'json-schema/attributes/ref' 3 | 4 | module JSON 5 | class Schema 6 | class ExtendsAttribute < Attribute 7 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 8 | schemas = current_schema.schema['extends'] 9 | schemas = [schemas] if !schemas.is_a?(Array) 10 | schemas.each do |s| 11 | uri,schema = get_extended_uri_and_schema(s, current_schema, validator) 12 | if schema 13 | schema.validate(data, fragments, processor, options) 14 | elsif uri 15 | message = "The extended schema '#{uri.to_s}' cannot be found" 16 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 17 | else 18 | message = "The property '#{build_fragment(fragments)}' was not a valid schema" 19 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 20 | end 21 | end 22 | end 23 | 24 | def self.get_extended_uri_and_schema(s, current_schema, validator) 25 | uri,schema = nil,nil 26 | 27 | if s.is_a?(Hash) 28 | uri = current_schema.uri 29 | if s['$ref'] 30 | ref_uri,ref_schema = JSON::Schema::RefAttribute.get_referenced_uri_and_schema(s, current_schema, validator) 31 | if ref_schema 32 | if s.size == 1 # Check if anything else apart from $ref 33 | uri,schema = ref_uri,ref_schema 34 | else 35 | s = s.dup 36 | s.delete '$ref' 37 | s = ref_schema.schema.merge(s) 38 | end 39 | end 40 | end 41 | schema ||= JSON::Schema.new(s,uri,validator) 42 | end 43 | 44 | [uri,schema] 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/merge_missing_values_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class MergeMissingValuesTest < Minitest::Test 4 | def test_merge_missing_values_for_string 5 | original = 'foo' 6 | updated = 'foo' 7 | JSON::Validator.merge_missing_values(updated, original) 8 | assert_equal('foo', original) 9 | end 10 | 11 | def test_merge_missing_values_for_empty_array 12 | original = [] 13 | updated = [] 14 | JSON::Validator.merge_missing_values(updated, original) 15 | assert_equal([], original) 16 | end 17 | 18 | def test_merge_missing_values_for_empty_hash 19 | original = {} 20 | updated = {} 21 | JSON::Validator.merge_missing_values(updated, original) 22 | assert_equal({}, original) 23 | end 24 | 25 | def test_merge_missing_values_for_new_values 26 | original = {:hello => 'world'} 27 | updated = {'hello' => 'world', 'foo' => 'bar'} 28 | JSON::Validator.merge_missing_values(updated, original) 29 | assert_equal({:hello => 'world', 'foo' => 'bar'}, original) 30 | end 31 | 32 | def test_merge_missing_values_for_nested_array 33 | original = [:hello, 'world', 1, 2, 3, {:foo => :bar, 'baz' => 'qux'}] 34 | updated = ['hello', 'world', 1, 2, 3, {'foo' => 'bar', 'baz' => 'qux', 'this_is' => 'new'}] 35 | JSON::Validator.merge_missing_values(updated, original) 36 | assert_equal([:hello, 'world', 1, 2, 3, {:foo => :bar, 'baz' => 'qux', 'this_is' => 'new'}], original) 37 | end 38 | 39 | def test_merge_missing_values_for_nested_hash 40 | original = {:hello => 'world', :foo => ['bar', :baz, {:uno => {:due => 3}}]} 41 | updated = {'hello' => 'world', 'foo' => ['bar', 'baz', {'uno' => {'due' => 3, 'this_is' => 'new'}}], 'ack' => 'sed'} 42 | JSON::Validator.merge_missing_values(updated, original) 43 | assert_equal({:hello => 'world', :foo => ['bar', :baz, {:uno => {:due => 3, 'this_is' => 'new'}}], 'ack' => 'sed'}, original) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/uri_parsing_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require File.expand_path('../support/test_helper', __FILE__) 3 | 4 | class UriParsingTest < Minitest::Test 5 | def test_asian_characters 6 | schema = { 7 | "$schema"=> "http://json-schema.org/draft-04/schema#", 8 | "id"=> "http://俺:鍵@例え.テスト/p?条件#ここ#", 9 | "type" => "object", 10 | "required" => ["a"], 11 | "properties" => { 12 | "a" => { 13 | "id" => "a", 14 | "type" => "integer" 15 | } 16 | } 17 | } 18 | data = { "a" => 5 } 19 | assert_valid schema, data 20 | end 21 | 22 | def test_schema_ref_with_empty_fragment 23 | schema = { 24 | "$schema" => "http://json-schema.org/draft-04/schema#", 25 | "type" => "object", 26 | "required" => ["names"], 27 | "properties"=> { 28 | "names"=> { 29 | "type"=> "array", 30 | "items"=> { 31 | "anyOf"=> [ 32 | { "$ref" => "test/schemas/ref john with spaces schema.json#" }, 33 | ] 34 | } 35 | } 36 | } 37 | } 38 | data = {"names" => [{"first" => "john"}]} 39 | assert_valid schema, data 40 | end 41 | 42 | def test_schema_ref_from_file_with_spaces 43 | schema = { 44 | "$schema" => "http://json-schema.org/draft-04/schema#", 45 | "type" => "object", 46 | "required" => ["names"], 47 | "properties"=> { 48 | "names"=> { 49 | "type"=> "array", 50 | "items"=> { 51 | "anyOf"=> [ 52 | { "$ref" => "test/schemas/ref john with spaces schema.json" } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | data = {"names" => [{"first" => "john"}]} 59 | assert_valid schema, data 60 | end 61 | 62 | def test_schema_from_file_with_spaces 63 | data = {"first" => "john"} 64 | schema = "test/schemas/ref john with spaces schema.json" 65 | assert_valid schema, data 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/fragment_validation_with_ref_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class FragmentValidationWithRefTest < Minitest::Test 4 | def whole_schema 5 | { 6 | "$schema" => "http://json-schema.org/draft-04/schema#", 7 | "type" => "object", 8 | "definitions" => { 9 | "post" => { 10 | "type" => "object", 11 | "properties" => { 12 | "content" => { 13 | "type" => "string" 14 | }, 15 | "author" => { 16 | "type" => "string" 17 | } 18 | } 19 | }, 20 | "posts" => { 21 | "type" => "array", 22 | "items" => { 23 | "$ref" => "#/definitions/post" 24 | } 25 | } 26 | } 27 | } 28 | end 29 | 30 | def whole_schema_with_array 31 | { 32 | "$schema" => "http://json-schema.org/draft-04/schema#", 33 | "type" => "object", 34 | "definitions" => { 35 | "omg" => { 36 | "links" => [ 37 | { 38 | "type" => "object", 39 | "schema" => { 40 | "properties" => { 41 | "content" => { 42 | "type" => "string" 43 | }, 44 | "author" => { 45 | "type" => "string" 46 | } 47 | }, 48 | "required" => ["content", "author"] 49 | } 50 | } 51 | ] 52 | } 53 | } 54 | } 55 | end 56 | 57 | def test_validation_of_fragment 58 | data = [{"content" => "ohai", "author" => "Bob"}] 59 | assert_valid whole_schema, data, :fragment => "#/definitions/posts" 60 | end 61 | 62 | def test_validation_of_fragment_with_array 63 | data = {"content" => "ohai", "author" => "Bob"} 64 | assert_valid(whole_schema_with_array, data, 65 | :fragment => "#/definitions/omg/links/0/schema") 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/json-schema/schema.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module JSON 4 | class Schema 5 | 6 | attr_accessor :schema, :uri, :validator 7 | 8 | def initialize(schema,uri,parent_validator=nil) 9 | @schema = schema 10 | @uri = uri 11 | 12 | # If there is an ID on this schema, use it to generate the URI 13 | if @schema['id'] && @schema['id'].kind_of?(String) 14 | temp_uri = JSON::Util::URI.parse(@schema['id']) 15 | if temp_uri.relative? 16 | temp_uri = uri.join(temp_uri) 17 | end 18 | @uri = temp_uri 19 | end 20 | @uri = JSON::Util::URI.strip_fragment(@uri) 21 | 22 | # If there is a $schema on this schema, use it to determine which validator to use 23 | if @schema['$schema'] 24 | @validator = JSON::Validator.validator_for_uri(@schema['$schema']) 25 | elsif parent_validator 26 | @validator = parent_validator 27 | else 28 | @validator = JSON::Validator.default_validator 29 | end 30 | end 31 | 32 | def validate(data, fragments, processor, options = {}) 33 | @validator.validate(self, data, fragments, processor, options) 34 | end 35 | 36 | def self.stringify(schema) 37 | case schema 38 | when Hash then 39 | Hash[schema.map { |key, value| [key.to_s, stringify(schema[key])] }] 40 | when Array then 41 | schema.map do |schema_item| 42 | stringify(schema_item) 43 | end 44 | when Symbol then 45 | schema.to_s 46 | else 47 | schema 48 | end 49 | end 50 | 51 | # @return [JSON::Schema] a new schema matching an array whose items all match this schema. 52 | def to_array_schema 53 | array_schema = { 'type' => 'array', 'items' => schema } 54 | array_schema['$schema'] = schema['$schema'] unless schema['$schema'].nil? 55 | self.class.new(array_schema, uri, validator) 56 | end 57 | 58 | def to_s 59 | @schema.to_json 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/anyof.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class AnyOfAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | # Create a hash to hold errors that are generated during validation 8 | errors = Hash.new { |hsh, k| hsh[k] = [] } 9 | valid = false 10 | 11 | original_data = data.is_a?(Hash) ? data.clone : data 12 | 13 | current_schema.schema['anyOf'].each_with_index do |element, schema_index| 14 | schema = JSON::Schema.new(element,current_schema.uri,validator) 15 | 16 | # We're going to add a little cruft here to try and maintain any validation errors that occur in the anyOf 17 | # We'll handle this by keeping an error count before and after validation, extracting those errors and pushing them onto a union error 18 | pre_validation_error_count = validation_errors(processor).count 19 | 20 | begin 21 | schema.validate(data,fragments,processor,options) 22 | valid = true 23 | rescue ValidationError 24 | # We don't care that these schemas don't validate - we only care that one validated 25 | end 26 | 27 | diff = validation_errors(processor).count - pre_validation_error_count 28 | valid = false if diff > 0 29 | while diff > 0 30 | diff = diff - 1 31 | errors["anyOf ##{schema_index}"].push(validation_errors(processor).pop) 32 | end 33 | 34 | break if valid 35 | 36 | data = original_data 37 | end 38 | 39 | if !valid 40 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match one or more of the required schemas" 41 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 42 | validation_errors(processor).last.sub_errors = errors 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/support/object_validation.rb: -------------------------------------------------------------------------------- 1 | module ObjectValidation 2 | module AdditionalPropertiesTests 3 | def test_additional_properties_false 4 | schema = { 5 | 'properties' => { 6 | 'a' => { 'type' => 'integer' } 7 | }, 8 | 'additionalProperties' => false 9 | } 10 | 11 | assert_valid schema, {'a' => 1} 12 | refute_valid schema, {'a' => 1, 'b' => 2} 13 | end 14 | 15 | def test_additional_properties_schema 16 | schema = { 17 | 'properties' => { 18 | 'a' => { 'type' => 'integer' } 19 | }, 20 | 'additionalProperties' => { 'type' => 'string' } 21 | } 22 | 23 | assert_valid schema, {'a' => 1} 24 | assert_valid schema, {'a' => 1, 'b' => 'hi'} 25 | refute_valid schema, {'a' => 1, 'b' => 2} 26 | end 27 | end 28 | 29 | module PatternPropertiesTests 30 | def test_pattern_properties 31 | schema = { 32 | 'patternProperties' => { 33 | "\\d+ taco" => { 'type' => 'integer' } 34 | } 35 | } 36 | 37 | assert_valid schema, {'1 taco' => 1, '20 taco' => 20} 38 | assert_valid schema, {'foo' => true, '1 taco' => 1} 39 | refute_valid schema, {'1 taco' => 'yum'} 40 | end 41 | 42 | def test_pattern_properties_additional_properties_false 43 | schema = { 44 | 'patternProperties' => { 45 | "\\d+ taco" => { 'type' => 'integer' } 46 | }, 47 | 'additionalProperties' => false 48 | } 49 | 50 | assert_valid schema, {'1 taco' => 1} 51 | refute_valid schema, {'1 taco' => 'yum'} 52 | refute_valid schema, {'1 taco' => 1, 'foo' => true} 53 | end 54 | 55 | def test_pattern_properties_additional_properties_schema 56 | schema = { 57 | 'patternProperties' => { 58 | "\\d+ taco" => { 'type' => 'integer' } 59 | }, 60 | 'additionalProperties' => { 'type' => 'string' } 61 | } 62 | 63 | assert_valid schema, {'1 taco' => 1} 64 | assert_valid schema, {'1 taco' => 1, 'foo' => 'bar'} 65 | refute_valid schema, {'1 taco' => 1, 'foo' => 2} 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/common_test_suite_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | require 'json' 3 | 4 | class CommonTestSuiteTest < Minitest::Test 5 | TEST_DIR = File.expand_path('../test-suite/tests', __FILE__) 6 | 7 | IGNORED_TESTS = YAML.load_file(File.expand_path('../support/test_suite_ignored_tests.yml', __FILE__)) 8 | 9 | def setup 10 | Dir["#{TEST_DIR}/../remotes/**/*.json"].each do |path| 11 | schema = path.sub(%r{^.*/remotes/}, '') 12 | stub_request(:get, "http://localhost:1234/#{schema}"). 13 | to_return(:body => File.read(path), :status => 200) 14 | end 15 | end 16 | 17 | def self.skip?(current_test, file_path) 18 | skipped_in_file = file_path.chomp('.json').split('/').inject(IGNORED_TESTS) do |ignored, path_component| 19 | ignored.nil? ? nil : ignored[path_component] 20 | end 21 | 22 | !skipped_in_file.nil? && (skipped_in_file == :all || skipped_in_file.include?(current_test)) 23 | end 24 | 25 | Dir["#{TEST_DIR}/*"].each do |suite| 26 | version = File.basename(suite).to_sym 27 | Dir["#{suite}/**/*.json"].each do |tfile| 28 | test_list = JSON.parse(File.read(tfile)) 29 | rel_file = tfile[TEST_DIR.length+1..-1] 30 | 31 | test_list.each do |test| 32 | schema = test["schema"] 33 | base_description = test["description"] 34 | 35 | test["tests"].each do |t| 36 | full_description = "#{base_description}/#{t['description']}" 37 | 38 | next if rel_file.include?('/optional/') && skip?(full_description, rel_file) 39 | 40 | err_id = "#{rel_file}: #{full_description}" 41 | define_method("test_#{err_id}") do 42 | skip if self.class.skip?(full_description, rel_file) 43 | 44 | errors = JSON::Validator.fully_validate(schema, 45 | t["data"], 46 | :parse_data => false, 47 | :validate_schema => true, 48 | :version => version 49 | ) 50 | assert_equal t["valid"], errors.empty?, "Common test suite case failed: #{err_id}" 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft3.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | 6 | class Draft3 < Validator 7 | def initialize 8 | super 9 | @attributes = { 10 | "type" => JSON::Schema::TypeAttribute, 11 | "disallow" => JSON::Schema::DisallowAttribute, 12 | "format" => JSON::Schema::FormatAttribute, 13 | "maximum" => JSON::Schema::MaximumAttribute, 14 | "minimum" => JSON::Schema::MinimumAttribute, 15 | "minItems" => JSON::Schema::MinItemsAttribute, 16 | "maxItems" => JSON::Schema::MaxItemsAttribute, 17 | "uniqueItems" => JSON::Schema::UniqueItemsAttribute, 18 | "minLength" => JSON::Schema::MinLengthAttribute, 19 | "maxLength" => JSON::Schema::MaxLengthAttribute, 20 | "divisibleBy" => JSON::Schema::DivisibleByAttribute, 21 | "enum" => JSON::Schema::EnumAttribute, 22 | "properties" => JSON::Schema::PropertiesAttribute, 23 | "pattern" => JSON::Schema::PatternAttribute, 24 | "patternProperties" => JSON::Schema::PatternPropertiesAttribute, 25 | "additionalProperties" => JSON::Schema::AdditionalPropertiesAttribute, 26 | "items" => JSON::Schema::ItemsAttribute, 27 | "additionalItems" => JSON::Schema::AdditionalItemsAttribute, 28 | "dependencies" => JSON::Schema::DependenciesAttribute, 29 | "extends" => JSON::Schema::ExtendsAttribute, 30 | "$ref" => JSON::Schema::RefAttribute 31 | } 32 | @default_formats = { 33 | 'date-time' => DateTimeFormat, 34 | 'date' => DateFormat, 35 | 'ip-address' => IP4Format, 36 | 'ipv6' => IP6Format, 37 | 'time' => TimeFormat, 38 | 'uri' => UriFormat 39 | } 40 | @formats = @default_formats.clone 41 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-03/schema#") 42 | @names = ["draft3", "http://json-schema.org/draft-03/schema#"] 43 | @metaschema_name = "draft-03.json" 44 | end 45 | 46 | JSON::Validator.register_validator(self.new) 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/oneof.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class OneOfAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | errors = Hash.new { |hsh, k| hsh[k] = [] } 8 | 9 | validation_error_count = 0 10 | one_of = current_schema.schema['oneOf'] 11 | 12 | original_data = data.is_a?(Hash) ? data.clone : data 13 | success_data = nil 14 | 15 | valid = false 16 | 17 | one_of.each_with_index do |element, schema_index| 18 | schema = JSON::Schema.new(element,current_schema.uri,validator) 19 | pre_validation_error_count = validation_errors(processor).count 20 | begin 21 | schema.validate(data,fragments,processor,options) 22 | success_data = data.is_a?(Hash) ? data.clone : data 23 | valid = true 24 | rescue ValidationError 25 | valid = false 26 | end 27 | 28 | diff = validation_errors(processor).count - pre_validation_error_count 29 | valid = false if diff > 0 30 | validation_error_count += 1 if !valid 31 | while diff > 0 32 | diff = diff - 1 33 | errors["oneOf ##{schema_index}"].push(validation_errors(processor).pop) 34 | end 35 | data = original_data 36 | end 37 | 38 | 39 | 40 | if validation_error_count == one_of.length - 1 41 | data = success_data 42 | return 43 | end 44 | 45 | if validation_error_count == one_of.length 46 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match any of the required schemas" 47 | else 48 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} matched more than one of the required schemas" 49 | end 50 | 51 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) if message 52 | validation_errors(processor).last.sub_errors = errors if message 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/support/number_validation.rb: -------------------------------------------------------------------------------- 1 | module NumberValidation 2 | module MinMaxTests 3 | def test_minimum 4 | schema = { 5 | 'properties' => { 6 | 'a' => { 'minimum' => 5 } 7 | } 8 | } 9 | 10 | assert_valid schema, {'a' => 5} 11 | assert_valid schema, {'a' => 6} 12 | 13 | refute_valid schema, {'a' => 4} 14 | refute_valid schema, {'a' => 4.99999} 15 | 16 | # other types are disregarded 17 | assert_valid schema, {'a' => 'str'} 18 | end 19 | 20 | def test_exclusive_minimum 21 | schema = { 22 | 'properties' => { 23 | 'a' => { 'minimum' => 5 }.merge(exclusive_minimum) 24 | } 25 | } 26 | 27 | assert_valid schema, {'a' => 6} 28 | assert_valid schema, {'a' => 5.0001} 29 | refute_valid schema, {'a' => 5} 30 | end 31 | 32 | def test_maximum 33 | schema = { 34 | 'properties' => { 35 | 'a' => { 'maximum' => 5 } 36 | } 37 | } 38 | 39 | assert_valid schema, {'a' => 4} 40 | assert_valid schema, {'a' => 5} 41 | 42 | refute_valid schema, {'a' => 6} 43 | refute_valid schema, {'a' => 5.0001} 44 | end 45 | 46 | def test_exclusive_maximum 47 | schema = { 48 | 'properties' => { 49 | 'a' => { 'maximum' => 5 }.merge(exclusive_maximum) 50 | } 51 | } 52 | 53 | assert_valid schema, {'a' => 4} 54 | assert_valid schema, {'a' => 4.99999} 55 | refute_valid schema, {'a' => 5} 56 | end 57 | end 58 | 59 | # draft3 introduced `divisibleBy`, renamed to `multipleOf` in draft4. 60 | # Favor the newer name, but the behavior should be identical. 61 | module MultipleOfTests 62 | def multiple_of 63 | 'multipleOf' 64 | end 65 | 66 | def test_multiple_of 67 | schema = { 68 | 'properties' => { 69 | 'a' => { multiple_of => 1.1 } 70 | } 71 | } 72 | 73 | assert_valid schema, {'a' => 0} 74 | 75 | assert_valid schema, {'a' => 2.2} 76 | refute_valid schema, {'a' => 3.4} 77 | 78 | # other types are disregarded 79 | assert_valid schema, {'a' => 'hi'} 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/support/type_validation.rb: -------------------------------------------------------------------------------- 1 | module TypeValidation 2 | # The draft4 schema refers to the JSON types as 'simple types'; 3 | # see draft4#/definitions/simpleTypes 4 | module SimpleTypeTests 5 | TYPES = { 6 | 'integer' => 5, 7 | 'number' => 5.0, 8 | 'string' => 'str', 9 | 'boolean' => true, 10 | 'object' => {}, 11 | 'array' => [], 12 | 'null' => nil 13 | } 14 | 15 | TYPES.each do |name, value| 16 | other_values = TYPES.values.reject { |v| v == value } 17 | 18 | define_method(:"test_#{name}_type_property") do 19 | schema = { 20 | 'properties' => { 'a' => { 'type' => name } } 21 | } 22 | assert_valid schema, {'a' => value} 23 | 24 | other_values.each do |other_value| 25 | refute_valid schema, {'a' => other_value} 26 | end 27 | end 28 | 29 | define_method(:"test_#{name}_type_value") do 30 | schema = { 'type' => name } 31 | assert_valid schema, value 32 | 33 | other_values.each do |other_value| 34 | schema = { 'type' => name } 35 | refute_valid schema, other_value 36 | end 37 | end 38 | end 39 | 40 | def test_type_union 41 | schema = { 'type' => ['integer', 'string'] } 42 | assert_valid schema, 5 43 | assert_valid schema, 'str' 44 | refute_valid schema, nil 45 | refute_valid schema, [5, 'str'] 46 | end 47 | end 48 | 49 | # The draft1..3 schemas support an additional type, `any`. 50 | module AnyTypeTests 51 | def test_any_type 52 | schema = { 'type' => 'any' } 53 | 54 | SimpleTypeTests::TYPES.values.each do |value| 55 | assert_valid schema, value 56 | end 57 | end 58 | end 59 | 60 | # The draft1..3 schemas support schemas as values for `type`. 61 | module SchemaUnionTypeTests 62 | def test_union_type_with_schemas 63 | schema = { 64 | 'properties' => { 65 | 'a' => { 66 | 'type' => [ 67 | {'type' => 'string'}, 68 | {'type' => 'object', 'properties' => { 'b' => { 'type' => 'integer' }}} 69 | ] 70 | } 71 | } 72 | } 73 | 74 | assert_valid schema, {'a' => 'test'} 75 | refute_valid schema, {'a' => 5} 76 | 77 | assert_valid schema, {'a' => {'b' => 5}} 78 | refute_valid schema, {'a' => {'b' => 'taco'}} 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/fragment_resolution_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class FragmentResolutionTest < Minitest::Test 4 | def test_fragment_resolution 5 | schema = { 6 | "$schema" => "http://json-schema.org/draft-04/schema#", 7 | "required" => ["a"], 8 | "properties" => { 9 | "a" => { 10 | "type" => "object", 11 | "properties" => { 12 | "b" => {"type" => "integer" } 13 | } 14 | } 15 | } 16 | } 17 | 18 | data = {"b" => 5} 19 | refute_valid schema, data 20 | assert_valid schema, data, :fragment => "#/properties/a" 21 | 22 | assert_raises JSON::Schema::SchemaError do 23 | JSON::Validator.validate!(schema,data,:fragment => "/properties/a") 24 | end 25 | 26 | assert_raises JSON::Schema::SchemaError do 27 | JSON::Validator.validate!(schema,data,:fragment => "#/properties/b") 28 | end 29 | end 30 | 31 | def test_odd_level_fragment_resolution 32 | schema = { 33 | "foo" => { 34 | "type" => "object", 35 | "required" => ["a"], 36 | "properties" => { 37 | "a" => {"type" => "integer"} 38 | } 39 | } 40 | } 41 | 42 | assert_valid schema, {"a" => 1}, :fragment => "#/foo" 43 | refute_valid schema, {}, :fragment => "#/foo" 44 | end 45 | 46 | def test_even_level_fragment_resolution 47 | schema = { 48 | "foo" => { 49 | "bar" => { 50 | "type" => "object", 51 | "required" => ["a"], 52 | "properties" => { 53 | "a" => {"type" => "integer"} 54 | } 55 | } 56 | } 57 | } 58 | 59 | assert_valid schema, {"a" => 1}, :fragment => "#/foo/bar" 60 | refute_valid schema, {}, :fragment => "#/foo/bar" 61 | end 62 | 63 | def test_array_fragment_resolution 64 | schema = { 65 | "type" => "object", 66 | "required" => ["a"], 67 | "properties" => { 68 | "a" => { 69 | "anyOf" => [ 70 | {"type" => "integer"}, 71 | {"type" => "string"} 72 | ] 73 | } 74 | } 75 | } 76 | 77 | refute_valid schema, "foo", :fragment => "#/properties/a/anyOf/0" 78 | assert_valid schema, "foo", :fragment => "#/properties/a/anyOf/1" 79 | 80 | assert_valid schema, 5, :fragment => "#/properties/a/anyOf/0" 81 | refute_valid schema, 5, :fragment => "#/properties/a/anyOf/1" 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/files_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class FilesTest < Minitest::Test 4 | 5 | # 6 | # These tests are ONLY run if there is an appropriate JSON backend parser available 7 | # 8 | 9 | def test_schema_from_file 10 | assert_valid schema_fixture_path('good_schema_1.json'), { "a" => 5 } 11 | refute_valid schema_fixture_path('good_schema_1.json'), { "a" => "bad" } 12 | end 13 | 14 | def test_data_from_file_v3 15 | schema = {"$schema" => "http://json-schema.org/draft-03/schema#","type" => "object", "properties" => {"a" => {"type" => "integer"}}} 16 | assert_valid schema, data_fixture_path('good_data_1.json'), :uri => true 17 | refute_valid schema, data_fixture_path('bad_data_1.json'), :uri => true 18 | end 19 | 20 | def test_data_from_json_v3 21 | schema = {"$schema" => "http://json-schema.org/draft-03/schema#","type" => "object", "properties" => {"a" => {"type" => "integer"}}} 22 | assert_valid schema, %Q({"a": 5}), :json => true 23 | refute_valid schema, %Q({"a": "poop"}), :json => true 24 | end 25 | 26 | def test_data_from_file_v4 27 | schema = {"$schema" => "http://json-schema.org/draft-04/schema#","type" => "object", "properties" => {"a" => {"type" => "integer"}}} 28 | assert_valid schema, data_fixture_path('good_data_1.json'), :uri => true 29 | refute_valid schema, data_fixture_path('bad_data_1.json'), :uri => true 30 | end 31 | 32 | def test_data_from_json_v4 33 | schema = {"$schema" => "http://json-schema.org/draft-04/schema#","type" => "object", "properties" => {"a" => {"type" => "integer"}}} 34 | assert_valid schema, %Q({"a": 5}), :json => true 35 | refute_valid schema, %Q({"a": "poop"}), :json => true 36 | end 37 | 38 | def test_both_from_file 39 | assert_valid schema_fixture_path('good_schema_1.json'), data_fixture_path('good_data_1.json'), :uri => true 40 | refute_valid schema_fixture_path('good_schema_1.json'), data_fixture_path('bad_data_1.json'), :uri => true 41 | end 42 | 43 | def test_file_ref 44 | assert_valid schema_fixture_path('good_schema_2.json'), { "b" => { "a" => 5 } } 45 | refute_valid schema_fixture_path('good_schema_1.json'), { "b" => { "a" => "boo" } } 46 | end 47 | 48 | def test_file_extends 49 | assert_valid schema_fixture_path('good_schema_extends1.json'), { "a" => 5 } 50 | assert_valid schema_fixture_path('good_schema_extends2.json'), { "a" => 5, "b" => { "a" => 5 } } 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft6.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | 6 | class Draft6 < Validator 7 | def initialize 8 | super 9 | @attributes = { 10 | "type" => JSON::Schema::TypeV4Attribute, 11 | "allOf" => JSON::Schema::AllOfAttribute, 12 | "anyOf" => JSON::Schema::AnyOfAttribute, 13 | "oneOf" => JSON::Schema::OneOfAttribute, 14 | "not" => JSON::Schema::NotAttribute, 15 | "disallow" => JSON::Schema::DisallowAttribute, 16 | "format" => JSON::Schema::FormatAttribute, 17 | "maximum" => JSON::Schema::MaximumAttribute, 18 | "minimum" => JSON::Schema::MinimumAttribute, 19 | "minItems" => JSON::Schema::MinItemsAttribute, 20 | "maxItems" => JSON::Schema::MaxItemsAttribute, 21 | "minProperties" => JSON::Schema::MinPropertiesAttribute, 22 | "maxProperties" => JSON::Schema::MaxPropertiesAttribute, 23 | "uniqueItems" => JSON::Schema::UniqueItemsAttribute, 24 | "minLength" => JSON::Schema::MinLengthAttribute, 25 | "maxLength" => JSON::Schema::MaxLengthAttribute, 26 | "multipleOf" => JSON::Schema::MultipleOfAttribute, 27 | "enum" => JSON::Schema::EnumAttribute, 28 | "properties" => JSON::Schema::PropertiesV4Attribute, 29 | "required" => JSON::Schema::RequiredAttribute, 30 | "pattern" => JSON::Schema::PatternAttribute, 31 | "patternProperties" => JSON::Schema::PatternPropertiesAttribute, 32 | "additionalProperties" => JSON::Schema::AdditionalPropertiesAttribute, 33 | "items" => JSON::Schema::ItemsAttribute, 34 | "additionalItems" => JSON::Schema::AdditionalItemsAttribute, 35 | "dependencies" => JSON::Schema::DependenciesV4Attribute, 36 | "extends" => JSON::Schema::ExtendsAttribute, 37 | "$ref" => JSON::Schema::RefAttribute 38 | } 39 | @default_formats = { 40 | 'date-time' => DateTimeV4Format, 41 | 'ipv4' => IP4Format, 42 | 'ipv6' => IP6Format, 43 | 'uri' => UriFormat 44 | } 45 | @formats = @default_formats.clone 46 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft/schema#") 47 | @names = ["draft6", "http://json-schema.org/draft/schema#"] 48 | @metaschema_name = "draft-06.json" 49 | end 50 | 51 | JSON::Validator.register_validator(self.new) 52 | JSON::Validator.register_default_validator(self.new) 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | 6 | class Draft4 < Validator 7 | def initialize 8 | super 9 | @attributes = { 10 | "type" => JSON::Schema::TypeV4Attribute, 11 | "allOf" => JSON::Schema::AllOfAttribute, 12 | "anyOf" => JSON::Schema::AnyOfAttribute, 13 | "oneOf" => JSON::Schema::OneOfAttribute, 14 | "not" => JSON::Schema::NotAttribute, 15 | "disallow" => JSON::Schema::DisallowAttribute, 16 | "format" => JSON::Schema::FormatAttribute, 17 | "maximum" => JSON::Schema::MaximumAttribute, 18 | "minimum" => JSON::Schema::MinimumAttribute, 19 | "minItems" => JSON::Schema::MinItemsAttribute, 20 | "maxItems" => JSON::Schema::MaxItemsAttribute, 21 | "minProperties" => JSON::Schema::MinPropertiesAttribute, 22 | "maxProperties" => JSON::Schema::MaxPropertiesAttribute, 23 | "uniqueItems" => JSON::Schema::UniqueItemsAttribute, 24 | "minLength" => JSON::Schema::MinLengthAttribute, 25 | "maxLength" => JSON::Schema::MaxLengthAttribute, 26 | "multipleOf" => JSON::Schema::MultipleOfAttribute, 27 | "enum" => JSON::Schema::EnumAttribute, 28 | "properties" => JSON::Schema::PropertiesV4Attribute, 29 | "required" => JSON::Schema::RequiredAttribute, 30 | "pattern" => JSON::Schema::PatternAttribute, 31 | "patternProperties" => JSON::Schema::PatternPropertiesAttribute, 32 | "additionalProperties" => JSON::Schema::AdditionalPropertiesAttribute, 33 | "items" => JSON::Schema::ItemsAttribute, 34 | "additionalItems" => JSON::Schema::AdditionalItemsAttribute, 35 | "dependencies" => JSON::Schema::DependenciesV4Attribute, 36 | "extends" => JSON::Schema::ExtendsAttribute, 37 | "$ref" => JSON::Schema::RefAttribute 38 | } 39 | @default_formats = { 40 | 'date-time' => DateTimeV4Format, 41 | 'ipv4' => IP4Format, 42 | 'ipv6' => IP6Format, 43 | 'uri' => UriFormat 44 | } 45 | @formats = @default_formats.clone 46 | @uri = JSON::Util::URI.parse("http://json-schema.org/draft-04/schema#") 47 | @names = ["draft4", "http://json-schema.org/draft-04/schema#"] 48 | @metaschema_name = "draft-04.json" 49 | end 50 | 51 | JSON::Validator.register_validator(self.new) 52 | JSON::Validator.register_default_validator(self.new) 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/additionalproperties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | require 'json-schema/attributes/extends' 3 | 4 | module JSON 5 | class Schema 6 | class AdditionalPropertiesAttribute < Attribute 7 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 8 | schema = current_schema.schema 9 | return unless data.is_a?(Hash) && (schema['type'].nil? || schema['type'] == 'object') 10 | 11 | extra_properties = remove_valid_properties(data.keys, current_schema, validator) 12 | 13 | addprop = schema['additionalProperties'] 14 | if addprop.is_a?(Hash) 15 | matching_properties = extra_properties # & addprop.keys 16 | matching_properties.each do |key| 17 | additional_property_schema = JSON::Schema.new(addprop, current_schema.uri, validator) 18 | additional_property_schema.validate(data[key], fragments + [key], processor, options) 19 | end 20 | extra_properties -= matching_properties 21 | end 22 | 23 | if extra_properties.any? && (addprop == false || (addprop.is_a?(Hash) && !addprop.empty?)) 24 | message = "The property '#{build_fragment(fragments)}' contains additional properties #{extra_properties.inspect} outside of the schema when none are allowed" 25 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 26 | end 27 | end 28 | 29 | def self.remove_valid_properties(extra_properties, current_schema, validator) 30 | schema = current_schema.schema 31 | 32 | if schema['properties'] 33 | extra_properties = extra_properties - schema['properties'].keys 34 | end 35 | 36 | if schema['patternProperties'] 37 | schema['patternProperties'].each_key do |key| 38 | regexp = Regexp.new(key) 39 | extra_properties.reject! { |prop| regexp.match(prop) } 40 | end 41 | end 42 | 43 | if extended_schemas = schema['extends'] 44 | extended_schemas = [extended_schemas] unless extended_schemas.is_a?(Array) 45 | extended_schemas.each do |schema_value| 46 | _, extended_schema = JSON::Schema::ExtendsAttribute.get_extended_uri_and_schema(schema_value, current_schema, validator) 47 | if extended_schema 48 | extra_properties = remove_valid_properties(extra_properties, extended_schema, validator) 49 | end 50 | end 51 | end 52 | 53 | extra_properties 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/ref.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | require 'json-schema/errors/schema_error' 3 | require 'json-schema/util/uri' 4 | 5 | module JSON 6 | class Schema 7 | class RefAttribute < Attribute 8 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 9 | uri,schema = get_referenced_uri_and_schema(current_schema.schema, current_schema, validator) 10 | 11 | if schema 12 | schema.validate(data, fragments, processor, options) 13 | elsif uri 14 | message = "The referenced schema '#{uri.to_s}' cannot be found" 15 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 16 | else 17 | message = "The property '#{build_fragment(fragments)}' was not a valid schema" 18 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 19 | end 20 | end 21 | 22 | def self.get_referenced_uri_and_schema(s, current_schema, validator) 23 | uri,schema = nil,nil 24 | 25 | temp_uri = JSON::Util::URI.normalize_ref(s['$ref'], current_schema.uri) 26 | 27 | # Grab the parent schema from the schema list 28 | schema_key = temp_uri.to_s.split("#")[0] + "#" 29 | 30 | ref_schema = JSON::Validator.schema_for_uri(schema_key) 31 | 32 | if ref_schema 33 | # Perform fragment resolution to retrieve the appropriate level for the schema 34 | target_schema = ref_schema.schema 35 | fragments = JSON::Util::URI.parse(JSON::Util::URI.unescape_uri(temp_uri)).fragment.split("/") 36 | fragment_path = '' 37 | fragments.each do |fragment| 38 | if fragment && fragment != '' 39 | fragment = fragment.gsub('~0', '~').gsub('~1', '/') 40 | if target_schema.is_a?(Array) 41 | target_schema = target_schema[fragment.to_i] 42 | else 43 | target_schema = target_schema[fragment] 44 | end 45 | fragment_path = fragment_path + "/#{fragment}" 46 | if target_schema.nil? 47 | raise SchemaError.new("The fragment '#{fragment_path}' does not exist on schema #{ref_schema.uri.to_s}") 48 | end 49 | end 50 | end 51 | 52 | # We have the schema finally, build it and validate! 53 | uri = temp_uri 54 | schema = JSON::Schema.new(target_schema,temp_uri,validator) 55 | end 56 | 57 | [uri,schema] 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/extended_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class ExtendedSchemaTest < Minitest::Test 4 | class BitwiseAndAttribute < JSON::Schema::Attribute 5 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 6 | return unless data.is_a?(Integer) 7 | 8 | if data & current_schema.schema['bitwise-and'].to_i == 0 9 | message = "The property '#{build_fragment(fragments)}' did not evaluate to true when bitwise-AND'd with #{current_schema.schema['bitwise-and']}" 10 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 11 | end 12 | end 13 | end 14 | 15 | class ExtendedSchema < JSON::Schema::Draft3 16 | def initialize 17 | super 18 | @attributes["bitwise-and"] = BitwiseAndAttribute 19 | @names = ["http://test.com/test.json"] 20 | @uri = Addressable::URI.parse("http://test.com/test.json") 21 | @names = ["http://test.com/test.json"] 22 | end 23 | 24 | JSON::Validator.register_validator(ExtendedSchema.new) 25 | end 26 | 27 | def test_extended_schema_validation 28 | schema = { 29 | "$schema" => "http://test.com/test.json", 30 | "properties" => { 31 | "a" => { 32 | "bitwise-and" => 1 33 | }, 34 | "b" => { 35 | "type" => "string" 36 | } 37 | } 38 | } 39 | 40 | assert_valid schema, {"a" => 1, "b" => "taco"} 41 | refute_valid schema, {"a" => 0, "b" => "taco"} 42 | refute_valid schema, {"a" => 1, "b" => 5} 43 | end 44 | 45 | def test_extended_schema_validation_with_fragment 46 | schema = { 47 | "$schema" => "http://test.com/test.json", 48 | "definitions" => { 49 | "odd-a" => { 50 | "properties" => { 51 | "a" => { 52 | "bitwise-and" => 1 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | assert_valid schema, {"a" => 1}, :fragment => "#/definitions/odd-a" 60 | refute_valid schema, {"a" => 0}, :fragment => "#/definitions/odd-a" 61 | end 62 | 63 | def test_unextended_schema 64 | # Verify that using the original schema disregards the `bitwise-and` property 65 | schema = { 66 | "properties" => { 67 | "a" => { 68 | "bitwise-and" => 1 69 | }, 70 | "b" => { 71 | "type" => "string" 72 | } 73 | } 74 | } 75 | 76 | assert_valid schema, {"a" => 0, "b" => "taco"} 77 | assert_valid schema, {"a" => 1, "b" => "taco"} 78 | refute_valid schema, {"a" => 1, "b" => 5} 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/properties.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class PropertiesAttribute < Attribute 6 | def self.required?(schema, options) 7 | schema.fetch('required') { options[:strict] } 8 | end 9 | 10 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 11 | return unless data.is_a?(Hash) 12 | 13 | schema = current_schema.schema 14 | schema['properties'].each do |property, property_schema| 15 | property = property.to_s 16 | 17 | if !data.key?(property) && 18 | options[:insert_defaults] && 19 | property_schema.has_key?('default') && 20 | !property_schema['readonly'] 21 | default = property_schema['default'] 22 | data[property] = default.is_a?(Hash) ? default.clone : default 23 | end 24 | 25 | if required?(property_schema, options) && !data.has_key?(property) 26 | message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'" 27 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 28 | end 29 | 30 | if data.has_key?(property) 31 | expected_schema = JSON::Schema.new(property_schema, current_schema.uri, validator) 32 | expected_schema.validate(data[property], fragments + [property], processor, options) 33 | end 34 | end 35 | 36 | # When strict is true, ensure no undefined properties exist in the data 37 | return unless options[:strict] == true && !schema.key?('additionalProperties') 38 | 39 | diff = data.select do |k, v| 40 | k = k.to_s 41 | 42 | if schema.has_key?('patternProperties') 43 | match = false 44 | schema['patternProperties'].each do |property, property_schema| 45 | regexp = Regexp.new(property) 46 | if regexp.match(k) 47 | match = true 48 | break 49 | end 50 | end 51 | 52 | !schema['properties'].has_key?(k) && !match 53 | else 54 | !schema['properties'].has_key?(k) 55 | end 56 | end 57 | 58 | if diff.size > 0 59 | properties = diff.keys.join(', ') 60 | message = "The property '#{build_fragment(fragments)}' contained undefined properties: '#{properties}'" 61 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/schema_reader_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class SchemaReaderTest < Minitest::Test 4 | ADDRESS_SCHEMA_URI = 'http://json-schema.org/address' 5 | ADDRESS_SCHEMA_PATH = File.expand_path('../schemas/address_microformat.json', __FILE__) 6 | 7 | def stub_address_request(body = File.read(ADDRESS_SCHEMA_PATH)) 8 | stub_request(:get, ADDRESS_SCHEMA_URI). 9 | to_return(:body => body, :status => 200) 10 | end 11 | 12 | def test_accept_all_uris 13 | stub_address_request 14 | 15 | reader = JSON::Schema::Reader.new 16 | schema = reader.read(ADDRESS_SCHEMA_URI) 17 | 18 | assert_equal schema.uri, Addressable::URI.parse("#{ADDRESS_SCHEMA_URI}#") 19 | end 20 | 21 | def test_accept_all_files 22 | reader = JSON::Schema::Reader.new 23 | schema = reader.read(ADDRESS_SCHEMA_PATH) 24 | 25 | assert_equal schema.uri, Addressable::URI.convert_path(ADDRESS_SCHEMA_PATH + '#') 26 | end 27 | 28 | def test_refuse_all_uris 29 | reader = JSON::Schema::Reader.new(:accept_uri => false) 30 | refute reader.accept_uri?(Addressable::URI.parse('http://foo.com')) 31 | end 32 | 33 | def test_refuse_all_files 34 | reader = JSON::Schema::Reader.new(:accept_file => false) 35 | refute reader.accept_file?(Pathname.new('/foo/bar/baz')) 36 | end 37 | 38 | def test_accept_uri_proc 39 | reader = JSON::Schema::Reader.new( 40 | :accept_uri => proc { |uri| uri.host == 'json-schema.org' } 41 | ) 42 | 43 | assert reader.accept_uri?(Addressable::URI.parse('http://json-schema.org/address')) 44 | refute reader.accept_uri?(Addressable::URI.parse('http://sub.json-schema.org/address')) 45 | end 46 | 47 | def test_accept_file_proc 48 | test_root = Pathname.new(__FILE__).expand_path.dirname 49 | 50 | reader = JSON::Schema::Reader.new( 51 | :accept_file => proc { |path| path.to_s.start_with?(test_root.to_s) } 52 | ) 53 | 54 | assert reader.accept_file?(test_root.join('anything.json')) 55 | refute reader.accept_file?(test_root.join('..', 'anything.json')) 56 | end 57 | 58 | def test_file_scheme 59 | reader = JSON::Schema::Reader.new(:accept_uri => true, :accept_file => false) 60 | error = assert_raises(JSON::Schema::ReadRefused) do 61 | reader.read('file://' + ADDRESS_SCHEMA_PATH) 62 | end 63 | 64 | assert_equal(:file, error.type) 65 | assert_equal(ADDRESS_SCHEMA_PATH, error.location) 66 | assert_equal("Read of file at #{ADDRESS_SCHEMA_PATH} refused", error.message) 67 | end 68 | 69 | def test_parse_error 70 | stub_address_request('this is totally not valid JSON!') 71 | 72 | reader = JSON::Schema::Reader.new 73 | 74 | assert_raises(JSON::Schema::JsonParseError) do 75 | reader.read(ADDRESS_SCHEMA_URI) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | Please keep to the changelog format described on [keepachangelog.com](http://keepachangelog.com). 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [2.8.1] - 2018-10-14 9 | 10 | ### Changed 11 | - All limit classes are now stored in their own files in 'json-schema/attributes/limits' 12 | - All attribute classes are now stored in their own files in 'json-schema/attributes' 13 | 14 | ### Fixed 15 | - Corrected the draft6 schema id to `http://json-schema.org/draft/schema#` 16 | - Rescue URI error when initializing a data string that contains a colon 17 | - Fragments with an odd number of components no longer raise an `undefined method `validate'` 18 | error 19 | 20 | ## [2.8.0] - 2017-02-07 21 | 22 | ### Added 23 | - Ruby 2.4 support 24 | 25 | ### Changed 26 | - Made the `:clear_cache` option for `validate` also clear the URI parse cache 27 | - Moved `JSON::Validator.absolutize_ref` and the ref manipulating code in 28 | `JSON::Schema::RefAttribute` into `JSON::Util::URI` 29 | - Made validation errors refer to json schema types not ruby types 30 | 31 | ### Deprecated 32 | - `JSON::Validator#validator_for` in favor of `JSON::Validator#validator_for_uri` 33 | - `JSON::Validator.validate2` in favor of `JSON::Validator.validate!` 34 | - `JSON::Schema::Validator#extend_schema_definition` in favour of subclassing 35 | 36 | ## [2.7.0] - 2016-09-29 37 | 38 | ### Fixed 39 | - Made sure we really do clear the cache when instructed to 40 | - It's now possible to use reserved words in property names 41 | - Removed support for setting "extends" to a string (it's invalid json-schema - use a "$ref" instead) 42 | - Relaxed 'items' and 'allowedItems' validation to permit arrays to pass even 43 | when they contain fewer elements than the 'items' array. To require full tuples, 44 | use 'minItems'. 45 | 46 | ### Changed 47 | - Made all `validate*` methods on `JSON::Validator` ultimately call `validate!` 48 | - Updated addressable dependency to 2.4.0 49 | - Attached failed `uri` or `pathname` to read errors for more meaning 50 | 51 | ## [2.6.2] - 2016-05-13 52 | 53 | ### Fixed 54 | - Made it possible to include colons in a $ref 55 | 56 | ### Changed 57 | - Reformatted examples in the readme 58 | 59 | ## [2.6.1] - 2016-02-26 60 | 61 | ### Fixed 62 | - Made sure schemas of an unrecognized type raise a SchemaParseError (not Name error) 63 | 64 | ### Changed 65 | - Readme was converted from textile to markdown 66 | 67 | ## [2.6.0] - 2016-01-08 68 | 69 | ### Added 70 | - Added a changelog 71 | 72 | ### Changed 73 | - Improved performance by caching the parsing and normalization of URIs 74 | - Made validation failures raise a `JSON::Schema::SchemaParseError` and data 75 | loading failures a `JSON::Schema::JsonLoadError` 76 | -------------------------------------------------------------------------------- /test/draft2_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class Draft2Test < Minitest::Test 4 | def validation_errors(schema, data, options) 5 | super(schema, data, :version => :draft2) 6 | end 7 | 8 | def exclusive_minimum 9 | { 'minimumCanEqual' => false } 10 | end 11 | 12 | def exclusive_maximum 13 | { 'maximumCanEqual' => false } 14 | end 15 | 16 | def multiple_of 17 | 'divisibleBy' 18 | end 19 | 20 | include ArrayValidation::ItemsTests 21 | include ArrayValidation::UniqueItemsTests 22 | 23 | include EnumValidation::General 24 | include EnumValidation::V1_V2 25 | 26 | include NumberValidation::MinMaxTests 27 | include NumberValidation::MultipleOfTests 28 | 29 | include ObjectValidation::AdditionalPropertiesTests 30 | 31 | include StrictValidation 32 | 33 | include StringValidation::ValueTests 34 | include StringValidation::FormatTests 35 | include StringValidation::DateAndTimeFormatTests 36 | 37 | include TypeValidation::SimpleTypeTests 38 | include TypeValidation::AnyTypeTests 39 | include TypeValidation::SchemaUnionTypeTests 40 | 41 | def test_optional 42 | # Set up the default datatype 43 | schema = { 44 | "properties" => { 45 | "a" => {"type" => "string"} 46 | } 47 | } 48 | data = {} 49 | 50 | refute_valid schema, data 51 | data['a'] = "Hello" 52 | assert_valid schema, data 53 | 54 | schema = { 55 | "properties" => { 56 | "a" => {"type" => "integer", "optional" => "true"} 57 | } 58 | } 59 | 60 | data = {} 61 | assert_valid schema, data 62 | end 63 | 64 | def test_disallow 65 | # Set up the default datatype 66 | schema = { 67 | "properties" => { 68 | "a" => {"disallow" => "integer"} 69 | } 70 | } 71 | 72 | data = { 73 | "a" => nil 74 | } 75 | 76 | 77 | data["a"] = 'string' 78 | assert_valid schema, data 79 | 80 | data["a"] = 5 81 | refute_valid schema, data 82 | 83 | 84 | schema["properties"]["a"]["disallow"] = ["integer","string"] 85 | data["a"] = 'string' 86 | refute_valid schema, data 87 | 88 | data["a"] = 5 89 | refute_valid schema, data 90 | 91 | data["a"] = false 92 | assert_valid schema, data 93 | end 94 | 95 | def test_format_datetime 96 | schema = { 97 | "type" => "object", 98 | "properties" => { "a" => {"type" => "string", "format" => "date-time"}} 99 | } 100 | 101 | assert_valid schema, {"a" => "2010-01-01T12:00:00Z"} 102 | refute_valid schema, {"a" => "2010-01-32T12:00:00Z"} 103 | refute_valid schema, {"a" => "2010-13-01T12:00:00Z"} 104 | refute_valid schema, {"a" => "2010-01-01T24:00:00Z"} 105 | refute_valid schema, {"a" => "2010-01-01T12:60:00Z"} 106 | refute_valid schema, {"a" => "2010-01-01T12:00:60Z"} 107 | refute_valid schema, {"a" => "2010-01-01T12:00:00z"} 108 | refute_valid schema, {"a" => "2010-01-0112:00:00Z"} 109 | refute_valid schema, {"a" => "2010-01-01T12:00:00Z\nabc"} 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /test/one_of_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class OneOfTest < Minitest::Test 4 | def test_one_of_links_schema 5 | schema = schema_fixture_path('one_of_ref_links_schema.json') 6 | data = data_fixture_path('one_of_ref_links_data.json') 7 | assert_valid schema, data 8 | end 9 | 10 | def test_one_of_with_string_patterns 11 | schema = { 12 | "$schema" => "http://json-schema.org/draft-04/schema#", 13 | "oneOf" => [ 14 | { 15 | "properties" => {"a" => {"type" => "string", "pattern" => "foo"}}, 16 | }, 17 | { 18 | "properties" => {"a" => {"type" => "string", "pattern" => "bar"}}, 19 | }, 20 | { 21 | "properties" => {"a" => {"type" => "string", "pattern" => "baz"}}, 22 | } 23 | ] 24 | } 25 | 26 | assert_valid schema, { "a" => "foo" } 27 | refute_valid schema, { "a" => "foobar" } 28 | assert_valid schema, { "a" => "baz" } 29 | refute_valid schema, { "a" => 5 } 30 | end 31 | 32 | def test_one_of_sub_errors 33 | schema = { 34 | "$schema" => "http://json-schema.org/draft-04/schema#", 35 | "oneOf" => [ 36 | { 37 | "properties" => {"a" => {"type" => "string", "pattern" => "foo"}}, 38 | }, 39 | { 40 | "properties" => {"a" => {"type" => "string", "pattern" => "bar"}}, 41 | }, 42 | { 43 | "properties" => {"a" => {"type" => "number", "minimum" => 10}}, 44 | } 45 | ] 46 | } 47 | 48 | errors = JSON::Validator.fully_validate(schema, { "a" => 5 }, :errors_as_objects => true) 49 | nested_errors = errors[0][:errors] 50 | assert_equal([:oneof_0,:oneof_1,:oneof_2], nested_errors.keys, 'should have nested errors for each allOf subschema') 51 | assert_match(/the property '#\/a' of type Integer did not match the following type: string/i, nested_errors[:oneof_0][0][:message]) 52 | assert_match(/the property '#\/a' did not have a minimum value of 10, inclusively/i, nested_errors[:oneof_2][0][:message]) 53 | end 54 | 55 | def test_one_of_sub_errors_message 56 | schema = { 57 | "$schema" => "http://json-schema.org/draft-04/schema#", 58 | "oneOf" => [ 59 | { 60 | "properties" => {"a" => {"type" => "string", "pattern" => "foo"}}, 61 | }, 62 | { 63 | "properties" => {"a" => {"type" => "string", "pattern" => "bar"}}, 64 | }, 65 | { 66 | "properties" => {"a" => {"type" => "number", "minimum" => 10}}, 67 | } 68 | ] 69 | } 70 | 71 | errors = JSON::Validator.fully_validate(schema, { "a" => 5 }) 72 | expected_message = """The property '#/' of type object did not match any of the required schemas. The schema specific errors were: 73 | 74 | - oneOf #0: 75 | - The property '#/a' of type integer did not match the following type: string 76 | - oneOf #1: 77 | - The property '#/a' of type integer did not match the following type: string 78 | - oneOf #2: 79 | - The property '#/a' did not have a minimum value of 10, inclusively""" 80 | 81 | assert_equal(expected_message, errors[0]) 82 | 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /test/support/strict_validation.rb: -------------------------------------------------------------------------------- 1 | module StrictValidation 2 | def test_strict_properties 3 | schema = { 4 | "$schema" => "http://json-schema.org/draft-04/schema#", 5 | "properties" => { 6 | "a" => {"type" => "string"}, 7 | "b" => {"type" => "string"} 8 | } 9 | } 10 | 11 | data = {"a" => "a"} 12 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 13 | 14 | data = {"b" => "b"} 15 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 16 | 17 | data = {"a" => "a", "b" => "b"} 18 | assert(JSON::Validator.validate(schema,data,:strict => true)) 19 | 20 | data = {"a" => "a", "b" => "b", "c" => "c"} 21 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 22 | end 23 | 24 | def test_strict_error_message 25 | schema = { :type => 'object', :properties => { :a => { :type => 'string' } } } 26 | data = { :a => 'abc', :b => 'abc' } 27 | errors = JSON::Validator.fully_validate(schema,data,:strict => true) 28 | assert_match("The property '#/' contained undefined properties: 'b' in schema", errors[0]) 29 | end 30 | 31 | def test_strict_properties_additional_props 32 | schema = { 33 | "$schema" => "http://json-schema.org/draft-04/schema#", 34 | "properties" => { 35 | "a" => {"type" => "string"}, 36 | "b" => {"type" => "string"} 37 | }, 38 | "additionalProperties" => {"type" => "integer"} 39 | } 40 | 41 | data = {"a" => "a"} 42 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 43 | 44 | data = {"b" => "b"} 45 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 46 | 47 | data = {"a" => "a", "b" => "b"} 48 | assert(JSON::Validator.validate(schema,data,:strict => true)) 49 | 50 | data = {"a" => "a", "b" => "b", "c" => "c"} 51 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 52 | 53 | data = {"a" => "a", "b" => "b", "c" => 3} 54 | assert(JSON::Validator.validate(schema,data,:strict => true)) 55 | end 56 | 57 | def test_strict_properties_pattern_props 58 | schema = { 59 | "properties" => { 60 | "a" => {"type" => "string"}, 61 | "b" => {"type" => "string"} 62 | }, 63 | "patternProperties" => {"\\d+ taco" => {"type" => "integer"}} 64 | } 65 | 66 | data = {"a" => "a"} 67 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 68 | 69 | data = {"b" => "b"} 70 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 71 | 72 | data = {"a" => "a", "b" => "b"} 73 | assert(JSON::Validator.validate(schema,data,:strict => true)) 74 | 75 | data = {"a" => "a", "b" => "b", "c" => "c"} 76 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 77 | 78 | data = {"a" => "a", "b" => "b", "c" => 3} 79 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 80 | 81 | data = {"a" => "a", "b" => "b", "23 taco" => 3} 82 | assert(JSON::Validator.validate(schema,data,:strict => true)) 83 | 84 | data = {"a" => "a", "b" => "b", "23 taco" => "cheese"} 85 | assert(!JSON::Validator.validate(schema,data,:strict => true)) 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/support/enum_validation.rb: -------------------------------------------------------------------------------- 1 | module EnumValidation 2 | module V1_V2 3 | def test_enum_optional 4 | schema = { 5 | "properties" => { 6 | "a" => {"enum" => [1,'boo',[1,2,3],{"a" => "b"}], "optional" => true} 7 | } 8 | } 9 | 10 | data = {} 11 | assert_valid schema, data 12 | end 13 | end 14 | 15 | module V3_V4 16 | def test_enum_optional 17 | schema = { 18 | "properties" => { 19 | "a" => {"enum" => [1,'boo',[1,2,3],{"a" => "b"}]} 20 | } 21 | } 22 | 23 | data = {} 24 | assert_valid schema, data 25 | end 26 | end 27 | 28 | module General 29 | def test_enum_general 30 | schema = { 31 | "properties" => { 32 | "a" => {"enum" => [1,'boo',[1,2,3],{"a" => "b"}]} 33 | } 34 | } 35 | 36 | data = { "a" => 1 } 37 | assert_valid schema, data 38 | 39 | data["a"] = 'boo' 40 | assert_valid schema, data 41 | 42 | data["a"] = [1,2,3] 43 | assert_valid schema, data 44 | 45 | data["a"] = {"a" => "b"} 46 | assert_valid schema, data 47 | 48 | data["a"] = 'taco' 49 | refute_valid schema, data 50 | end 51 | 52 | def test_enum_number_integer_includes_float 53 | schema = { 54 | "properties" => { 55 | "a" => { 56 | "type" => "number", 57 | "enum" => [0, 1, 2] 58 | } 59 | } 60 | } 61 | 62 | data = { "a" => 0 } 63 | assert_valid schema, data 64 | 65 | data["a"] = 0.0 66 | assert_valid schema, data 67 | 68 | data["a"] = 1 69 | assert_valid schema, data 70 | 71 | data["a"] = 1.0 72 | assert_valid schema, data 73 | end 74 | 75 | def test_enum_number_float_includes_integer 76 | schema = { 77 | "properties" => { 78 | "a" => { 79 | "type" => "number", 80 | "enum" => [0.0, 1.0, 2.0] 81 | } 82 | } 83 | } 84 | 85 | data = { "a" => 0.0 } 86 | assert_valid schema, data 87 | 88 | data["a"] = 0 89 | assert_valid schema, data 90 | 91 | 92 | data["a"] = 1.0 93 | assert_valid schema, data 94 | 95 | data["a"] = 1 96 | assert_valid schema, data 97 | end 98 | 99 | def test_enum_integer_excludes_float 100 | schema = { 101 | "properties" => { 102 | "a" => { 103 | "type" => "integer", 104 | "enum" => [0, 1, 2] 105 | } 106 | } 107 | } 108 | 109 | data = { "a" => 0 } 110 | assert_valid schema, data 111 | 112 | data["a"] = 0.0 113 | refute_valid schema, data 114 | 115 | 116 | data["a"] = 1 117 | assert_valid schema, data 118 | 119 | data["a"] = 1.0 120 | refute_valid schema, data 121 | end 122 | 123 | def test_enum_with_schema_validation 124 | schema = { 125 | "properties" => { 126 | "a" => {"enum" => [1,'boo',[1,2,3],{"a" => "b"}]} 127 | } 128 | } 129 | data = { "a" => 1 } 130 | assert_valid(schema, data, :validate_schema => true) 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /test/support/array_validation.rb: -------------------------------------------------------------------------------- 1 | module ArrayValidation 2 | module ItemsTests 3 | def test_items_single_schema 4 | schema = { 'items' => { 'type' => 'string' } } 5 | 6 | assert_valid schema, [] 7 | assert_valid schema, ['a'] 8 | assert_valid schema, ['a', 'b'] 9 | 10 | refute_valid schema, [1] 11 | refute_valid schema, ['a', 1] 12 | 13 | # other types are disregarded 14 | assert_valid schema, {'a' => 'foo'} 15 | end 16 | 17 | def test_items_multiple_schemas 18 | schema = { 19 | 'items' => [ 20 | { 'type' => 'string' }, 21 | { 'type' => 'integer' } 22 | ] 23 | } 24 | 25 | assert_valid schema, ['b', 1] 26 | assert_valid schema, ['b', 1, nil] 27 | refute_valid schema, [1, 'b'] 28 | assert_valid schema, [] 29 | assert_valid schema, ['b'] 30 | assert_valid schema, ['b', 1, 25] 31 | end 32 | 33 | def test_minitems 34 | schema = { 'minItems' => 1 } 35 | 36 | assert_valid schema, [1] 37 | assert_valid schema, [1, 2] 38 | refute_valid schema, [] 39 | 40 | # other types are disregarded 41 | assert_valid schema, 5 42 | end 43 | 44 | def test_maxitems 45 | schema = { 'maxItems' => 1 } 46 | 47 | assert_valid schema, [] 48 | assert_valid schema, [1] 49 | refute_valid schema, [1, 2] 50 | 51 | # other types are disregarded 52 | assert_valid schema, 5 53 | end 54 | end 55 | 56 | module AdditionalItemsTests 57 | def test_additional_items_false 58 | schema = { 59 | 'items' => [ 60 | { 'type' => 'integer' }, 61 | { 'type' => 'string' } 62 | ], 63 | 'additionalItems' => false 64 | } 65 | 66 | assert_valid schema, [1, 'string'] 67 | assert_valid schema, [1] 68 | assert_valid schema, [] 69 | refute_valid schema, [1, 'string', 2] 70 | refute_valid schema, ['string', 1] 71 | end 72 | 73 | def test_additional_items_schema 74 | schema = { 75 | 'items' => [ 76 | { 'type' => 'integer' }, 77 | { 'type' => 'string' } 78 | ], 79 | 'additionalItems' => { 'type' => 'integer' } 80 | } 81 | 82 | assert_valid schema, [1, 'string'] 83 | assert_valid schema, [1, 'string', 2] 84 | refute_valid schema, [1, 'string', 'string'] 85 | end 86 | end 87 | 88 | module UniqueItemsTests 89 | def test_unique_items 90 | schema = { 'uniqueItems' => true } 91 | 92 | assert_valid schema, [nil, 5] 93 | refute_valid schema, [nil, nil] 94 | 95 | assert_valid schema, [true, false] 96 | refute_valid schema, [true, true] 97 | 98 | assert_valid schema, [4, 4.1] 99 | refute_valid schema, [4, 4] 100 | 101 | assert_valid schema, ['a', 'ab'] 102 | refute_valid schema, ['a', 'a'] 103 | 104 | assert_valid schema, [[1], [2]] 105 | refute_valid schema, [[1], [1]] 106 | 107 | assert_valid schema, [{'b' => 1}, {'c' => 2}] 108 | assert_valid schema, [{'b' => 1}, {'c' => 1}] 109 | refute_valid schema, [{'b' => 1}, {'b' => 1}] 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/type.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class TypeAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | union = true 8 | if options[:disallow] 9 | types = current_schema.schema['disallow'] 10 | else 11 | types = current_schema.schema['type'] 12 | end 13 | 14 | if !types.is_a?(Array) 15 | types = [types] 16 | union = false 17 | end 18 | valid = false 19 | 20 | # Create a hash to hold errors that are generated during union validation 21 | union_errors = Hash.new { |hsh, k| hsh[k] = [] } 22 | 23 | types.each_with_index do |type, type_index| 24 | if type.is_a?(String) 25 | valid = data_valid_for_type?(data, type) 26 | elsif type.is_a?(Hash) && union 27 | # Validate as a schema 28 | schema = JSON::Schema.new(type,current_schema.uri,validator) 29 | 30 | # We're going to add a little cruft here to try and maintain any validation errors that occur in this union type 31 | # We'll handle this by keeping an error count before and after validation, extracting those errors and pushing them onto a union error 32 | pre_validation_error_count = validation_errors(processor).count 33 | 34 | begin 35 | schema.validate(data,fragments,processor,options.merge(:disallow => false)) 36 | valid = true 37 | rescue ValidationError 38 | # We don't care that these schemas don't validate - we only care that one validated 39 | end 40 | 41 | diff = validation_errors(processor).count - pre_validation_error_count 42 | valid = false if diff > 0 43 | while diff > 0 44 | diff = diff - 1 45 | union_errors["type ##{type_index}"].push(validation_errors(processor).pop) 46 | end 47 | end 48 | 49 | break if valid 50 | end 51 | 52 | if options[:disallow] 53 | return if !valid 54 | message = "The property '#{build_fragment(fragments)}' matched one or more of the following types: #{list_types(types)}" 55 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 56 | elsif !valid 57 | if union 58 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match one or more of the following types: #{list_types(types)}" 59 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 60 | validation_errors(processor).last.sub_errors = union_errors 61 | else 62 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match the following type: #{list_types(types)}" 63 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 64 | end 65 | end 66 | end 67 | 68 | def self.list_types(types) 69 | types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ') 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /resources/draft-01.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema" : "http://json-schema.org/draft-01/hyper-schema#", 3 | "id" : "http://json-schema.org/draft-01/schema#", 4 | "type" : "object", 5 | 6 | "properties" : { 7 | "type" : { 8 | "type" : ["string", "array"], 9 | "items" : { 10 | "type" : ["string", {"$ref" : "#"}] 11 | }, 12 | "optional" : true, 13 | "default" : "any" 14 | }, 15 | 16 | "properties" : { 17 | "type" : "object", 18 | "additionalProperties" : {"$ref" : "#"}, 19 | "optional" : true, 20 | "default" : {} 21 | }, 22 | 23 | "items" : { 24 | "type" : [{"$ref" : "#"}, "array"], 25 | "items" : {"$ref" : "#"}, 26 | "optional" : true, 27 | "default" : {} 28 | }, 29 | 30 | "optional" : { 31 | "type" : "boolean", 32 | "optional" : true, 33 | "default" : false 34 | }, 35 | 36 | "additionalProperties" : { 37 | "type" : [{"$ref" : "#"}, "boolean"], 38 | "optional" : true, 39 | "default" : {} 40 | }, 41 | 42 | "requires" : { 43 | "type" : ["string", {"$ref" : "#"}], 44 | "optional" : true 45 | }, 46 | 47 | "minimum" : { 48 | "type" : "number", 49 | "optional" : true 50 | }, 51 | 52 | "maximum" : { 53 | "type" : "number", 54 | "optional" : true 55 | }, 56 | 57 | "minimumCanEqual" : { 58 | "type" : "boolean", 59 | "optional" : true, 60 | "requires" : "minimum", 61 | "default" : true 62 | }, 63 | 64 | "maximumCanEqual" : { 65 | "type" : "boolean", 66 | "optional" : true, 67 | "requires" : "maximum", 68 | "default" : true 69 | }, 70 | 71 | "minItems" : { 72 | "type" : "integer", 73 | "optional" : true, 74 | "minimum" : 0, 75 | "default" : 0 76 | }, 77 | 78 | "maxItems" : { 79 | "type" : "integer", 80 | "optional" : true, 81 | "minimum" : 0 82 | }, 83 | 84 | "pattern" : { 85 | "type" : "string", 86 | "optional" : true, 87 | "format" : "regex" 88 | }, 89 | 90 | "minLength" : { 91 | "type" : "integer", 92 | "optional" : true, 93 | "minimum" : 0, 94 | "default" : 0 95 | }, 96 | 97 | "maxLength" : { 98 | "type" : "integer", 99 | "optional" : true 100 | }, 101 | 102 | "enum" : { 103 | "type" : "array", 104 | "optional" : true, 105 | "minItems" : 1 106 | }, 107 | 108 | "title" : { 109 | "type" : "string", 110 | "optional" : true 111 | }, 112 | 113 | "description" : { 114 | "type" : "string", 115 | "optional" : true 116 | }, 117 | 118 | "format" : { 119 | "type" : "string", 120 | "optional" : true 121 | }, 122 | 123 | "contentEncoding" : { 124 | "type" : "string", 125 | "optional" : true 126 | }, 127 | 128 | "default" : { 129 | "type" : "any", 130 | "optional" : true 131 | }, 132 | 133 | "maxDecimal" : { 134 | "type" : "integer", 135 | "optional" : true, 136 | "minimum" : 0 137 | }, 138 | 139 | "disallow" : { 140 | "type" : ["string", "array"], 141 | "items" : {"type" : "string"}, 142 | "optional" : true 143 | }, 144 | 145 | "extends" : { 146 | "type" : [{"$ref" : "#"}, "array"], 147 | "items" : {"$ref" : "#"}, 148 | "optional" : true, 149 | "default" : {} 150 | } 151 | }, 152 | 153 | "optional" : true, 154 | "default" : {} 155 | } -------------------------------------------------------------------------------- /lib/json-schema/util/uri.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | 3 | module JSON 4 | module Util 5 | module URI 6 | SUPPORTED_PROTOCOLS = %w(http https ftp tftp sftp ssh svn+ssh telnet nntp gopher wais ldap prospero) 7 | 8 | def self.normalized_uri(uri, base_path = Dir.pwd) 9 | @normalize_cache ||= {} 10 | normalized_uri = @normalize_cache[uri] 11 | 12 | if !normalized_uri 13 | normalized_uri = parse(uri) 14 | # Check for absolute path 15 | if normalized_uri.relative? 16 | data = normalized_uri 17 | data = File.join(base_path, data) if data.path[0,1] != "/" 18 | normalized_uri = file_uri(data) 19 | end 20 | @normalize_cache[uri] = normalized_uri.freeze 21 | end 22 | 23 | normalized_uri 24 | end 25 | 26 | def self.absolutize_ref(ref, base) 27 | ref_uri = strip_fragment(ref.dup) 28 | 29 | return ref_uri if ref_uri.absolute? 30 | return parse(base) if ref_uri.path.empty? 31 | 32 | uri = strip_fragment(base.dup).join(ref_uri.path) 33 | normalized_uri(uri) 34 | end 35 | 36 | def self.normalize_ref(ref, base) 37 | ref_uri = parse(ref) 38 | base_uri = parse(base) 39 | 40 | ref_uri.defer_validation do 41 | if ref_uri.relative? 42 | ref_uri.merge!(base_uri) 43 | 44 | # Check for absolute path 45 | path, fragment = ref.to_s.split("#") 46 | if path.nil? || path == '' 47 | ref_uri.path = base_uri.path 48 | elsif path[0,1] == "/" 49 | ref_uri.path = Pathname.new(path).cleanpath.to_s 50 | else 51 | ref_uri.join!(path) 52 | end 53 | 54 | ref_uri.fragment = fragment 55 | end 56 | 57 | ref_uri.fragment = "" if ref_uri.fragment.nil? || ref_uri.fragment.empty? 58 | end 59 | 60 | ref_uri 61 | end 62 | 63 | def self.parse(uri) 64 | if uri.is_a?(Addressable::URI) 65 | return uri.dup 66 | else 67 | @parse_cache ||= {} 68 | parsed_uri = @parse_cache[uri] 69 | if parsed_uri 70 | parsed_uri.dup 71 | else 72 | @parse_cache[uri] = Addressable::URI.parse(uri) 73 | end 74 | end 75 | rescue Addressable::URI::InvalidURIError => e 76 | raise JSON::Schema::UriError.new(e.message) 77 | end 78 | 79 | def self.strip_fragment(uri) 80 | parsed_uri = parse(uri) 81 | if parsed_uri.fragment.nil? || parsed_uri.fragment.empty? 82 | parsed_uri 83 | else 84 | parsed_uri.merge(:fragment => "") 85 | end 86 | end 87 | 88 | def self.file_uri(uri) 89 | parsed_uri = parse(uri) 90 | 91 | Addressable::URI.convert_path(parsed_uri.path) 92 | end 93 | 94 | def self.unescape_uri(uri) 95 | Addressable::URI.unescape(uri) 96 | end 97 | 98 | def self.unescaped_path(uri) 99 | parsed_uri = parse(uri) 100 | 101 | Addressable::URI.unescape(parsed_uri.path) 102 | end 103 | 104 | def self.clear_cache 105 | @parse_cache = {} 106 | @normalize_cache = {} 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /resources/draft-02.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema" : "http://json-schema.org/draft-02/hyper-schema#", 3 | "id" : "http://json-schema.org/draft-02/schema#", 4 | "type" : "object", 5 | 6 | "properties" : { 7 | "type" : { 8 | "type" : ["string", "array"], 9 | "items" : { 10 | "type" : ["string", {"$ref" : "#"}] 11 | }, 12 | "optional" : true, 13 | "uniqueItems" : true, 14 | "default" : "any" 15 | }, 16 | 17 | "properties" : { 18 | "type" : "object", 19 | "additionalProperties" : {"$ref" : "#"}, 20 | "optional" : true, 21 | "default" : {} 22 | }, 23 | 24 | "items" : { 25 | "type" : [{"$ref" : "#"}, "array"], 26 | "items" : {"$ref" : "#"}, 27 | "optional" : true, 28 | "default" : {} 29 | }, 30 | 31 | "optional" : { 32 | "type" : "boolean", 33 | "optional" : true, 34 | "default" : false 35 | }, 36 | 37 | "additionalProperties" : { 38 | "type" : [{"$ref" : "#"}, "boolean"], 39 | "optional" : true, 40 | "default" : {} 41 | }, 42 | 43 | "requires" : { 44 | "type" : ["string", {"$ref" : "#"}], 45 | "optional" : true 46 | }, 47 | 48 | "minimum" : { 49 | "type" : "number", 50 | "optional" : true 51 | }, 52 | 53 | "maximum" : { 54 | "type" : "number", 55 | "optional" : true 56 | }, 57 | 58 | "minimumCanEqual" : { 59 | "type" : "boolean", 60 | "optional" : true, 61 | "requires" : "minimum", 62 | "default" : true 63 | }, 64 | 65 | "maximumCanEqual" : { 66 | "type" : "boolean", 67 | "optional" : true, 68 | "requires" : "maximum", 69 | "default" : true 70 | }, 71 | 72 | "minItems" : { 73 | "type" : "integer", 74 | "optional" : true, 75 | "minimum" : 0, 76 | "default" : 0 77 | }, 78 | 79 | "maxItems" : { 80 | "type" : "integer", 81 | "optional" : true, 82 | "minimum" : 0 83 | }, 84 | 85 | "uniqueItems" : { 86 | "type" : "boolean", 87 | "optional" : true, 88 | "default" : false 89 | }, 90 | 91 | "pattern" : { 92 | "type" : "string", 93 | "optional" : true, 94 | "format" : "regex" 95 | }, 96 | 97 | "minLength" : { 98 | "type" : "integer", 99 | "optional" : true, 100 | "minimum" : 0, 101 | "default" : 0 102 | }, 103 | 104 | "maxLength" : { 105 | "type" : "integer", 106 | "optional" : true 107 | }, 108 | 109 | "enum" : { 110 | "type" : "array", 111 | "optional" : true, 112 | "minItems" : 1, 113 | "uniqueItems" : true 114 | }, 115 | 116 | "title" : { 117 | "type" : "string", 118 | "optional" : true 119 | }, 120 | 121 | "description" : { 122 | "type" : "string", 123 | "optional" : true 124 | }, 125 | 126 | "format" : { 127 | "type" : "string", 128 | "optional" : true 129 | }, 130 | 131 | "contentEncoding" : { 132 | "type" : "string", 133 | "optional" : true 134 | }, 135 | 136 | "default" : { 137 | "type" : "any", 138 | "optional" : true 139 | }, 140 | 141 | "divisibleBy" : { 142 | "type" : "number", 143 | "minimum" : 0, 144 | "minimumCanEqual" : false, 145 | "optional" : true, 146 | "default" : 1 147 | }, 148 | 149 | "disallow" : { 150 | "type" : ["string", "array"], 151 | "items" : {"type" : "string"}, 152 | "optional" : true, 153 | "uniqueItems" : true 154 | }, 155 | 156 | "extends" : { 157 | "type" : [{"$ref" : "#"}, "array"], 158 | "items" : {"$ref" : "#"}, 159 | "optional" : true, 160 | "default" : {} 161 | } 162 | }, 163 | 164 | "optional" : true, 165 | "default" : {} 166 | } -------------------------------------------------------------------------------- /test/draft1_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class Draft1Test < Minitest::Test 4 | def validation_errors(schema, data, options) 5 | super(schema, data, :version => :draft1) 6 | end 7 | 8 | def exclusive_minimum 9 | { 'minimumCanEqual' => false } 10 | end 11 | 12 | def exclusive_maximum 13 | { 'maximumCanEqual' => false } 14 | end 15 | 16 | include ArrayValidation::ItemsTests 17 | 18 | include EnumValidation::General 19 | include EnumValidation::V1_V2 20 | 21 | include NumberValidation::MinMaxTests 22 | 23 | include ObjectValidation::AdditionalPropertiesTests 24 | 25 | include StrictValidation 26 | 27 | include StringValidation::ValueTests 28 | include StringValidation::FormatTests 29 | include StringValidation::DateAndTimeFormatTests 30 | 31 | include TypeValidation::SimpleTypeTests 32 | include TypeValidation::AnyTypeTests 33 | include TypeValidation::SchemaUnionTypeTests 34 | 35 | def test_optional 36 | # Set up the default datatype 37 | schema = { 38 | "properties" => { 39 | "a" => {"type" => "string"} 40 | } 41 | } 42 | data = {} 43 | 44 | refute_valid schema, data 45 | data['a'] = "Hello" 46 | assert_valid schema, data 47 | 48 | schema = { 49 | "properties" => { 50 | "a" => {"type" => "integer", "optional" => "true"} 51 | } 52 | } 53 | 54 | data = {} 55 | assert_valid schema, data 56 | end 57 | 58 | def test_max_decimal 59 | # Set up the default datatype 60 | schema = { 61 | "properties" => { 62 | "a" => {"maxDecimal" => 2} 63 | } 64 | } 65 | 66 | data = { 67 | "a" => nil 68 | } 69 | 70 | data["a"] = 3.35 71 | assert_valid schema, data 72 | 73 | data["a"] = 3.455 74 | refute_valid schema, data 75 | 76 | 77 | schema["properties"]["a"]["maxDecimal"] = 0 78 | 79 | data["a"] = 4.0 80 | refute_valid schema, data 81 | 82 | data["a"] = 'boo' 83 | assert_valid schema, data 84 | 85 | data["a"] = 5 86 | assert_valid schema, data 87 | end 88 | 89 | 90 | 91 | def test_disallow 92 | # Set up the default datatype 93 | schema = { 94 | "properties" => { 95 | "a" => {"disallow" => "integer"} 96 | } 97 | } 98 | 99 | data = { 100 | "a" => nil 101 | } 102 | 103 | 104 | data["a"] = 'string' 105 | assert_valid schema, data 106 | 107 | data["a"] = 5 108 | refute_valid schema, data 109 | 110 | 111 | schema["properties"]["a"]["disallow"] = ["integer","string"] 112 | data["a"] = 'string' 113 | refute_valid schema, data 114 | 115 | data["a"] = 5 116 | refute_valid schema, data 117 | 118 | data["a"] = false 119 | assert_valid schema, data 120 | 121 | end 122 | 123 | def test_format_datetime 124 | schema = { 125 | "type" => "object", 126 | "properties" => { "a" => {"type" => "string", "format" => "date-time"}} 127 | } 128 | 129 | assert_valid schema, {"a" => "2010-01-01T12:00:00Z"} 130 | refute_valid schema, {"a" => "2010-01-32T12:00:00Z"} 131 | refute_valid schema, {"a" => "2010-13-01T12:00:00Z"} 132 | refute_valid schema, {"a" => "2010-01-01T24:00:00Z"} 133 | refute_valid schema, {"a" => "2010-01-01T12:60:00Z"} 134 | refute_valid schema, {"a" => "2010-01-01T12:00:60Z"} 135 | refute_valid schema, {"a" => "2010-01-01T12:00:00z"} 136 | refute_valid schema, {"a" => "2010-01-0112:00:00Z"} 137 | refute_valid schema, {"a" => "2010-01-01T12:00:00Z\nabc"} 138 | end 139 | 140 | end 141 | -------------------------------------------------------------------------------- /resources/draft-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema#", 3 | "id": "http://json-schema.org/draft-03/schema#", 4 | "type": "object", 5 | 6 | "properties": { 7 | "type": { 8 | "type": [ "string", "array" ], 9 | "items": { 10 | "type": [ "string", { "$ref": "#" } ] 11 | }, 12 | "uniqueItems": true, 13 | "default": "any" 14 | }, 15 | 16 | "properties": { 17 | "type": "object", 18 | "additionalProperties": { "$ref": "#" }, 19 | "default": {} 20 | }, 21 | 22 | "patternProperties": { 23 | "type": "object", 24 | "additionalProperties": { "$ref": "#" }, 25 | "default": {} 26 | }, 27 | 28 | "additionalProperties": { 29 | "type": [ { "$ref": "#" }, "boolean" ], 30 | "default": {} 31 | }, 32 | 33 | "items": { 34 | "type": [ { "$ref": "#" }, "array" ], 35 | "items": { "$ref": "#" }, 36 | "default": {} 37 | }, 38 | 39 | "additionalItems": { 40 | "type": [ { "$ref": "#" }, "boolean" ], 41 | "default": {} 42 | }, 43 | 44 | "required": { 45 | "type": "boolean", 46 | "default": false 47 | }, 48 | 49 | "dependencies": { 50 | "type": "object", 51 | "additionalProperties": { 52 | "type": [ "string", "array", { "$ref": "#" } ], 53 | "items": { 54 | "type": "string" 55 | } 56 | }, 57 | "default": {} 58 | }, 59 | 60 | "minimum": { 61 | "type": "number" 62 | }, 63 | 64 | "maximum": { 65 | "type": "number" 66 | }, 67 | 68 | "exclusiveMinimum": { 69 | "type": "boolean", 70 | "default": false 71 | }, 72 | 73 | "exclusiveMaximum": { 74 | "type": "boolean", 75 | "default": false 76 | }, 77 | 78 | "minItems": { 79 | "type": "integer", 80 | "minimum": 0, 81 | "default": 0 82 | }, 83 | 84 | "maxItems": { 85 | "type": "integer", 86 | "minimum": 0 87 | }, 88 | 89 | "uniqueItems": { 90 | "type": "boolean", 91 | "default": false 92 | }, 93 | 94 | "pattern": { 95 | "type": "string", 96 | "format": "regex" 97 | }, 98 | 99 | "minLength": { 100 | "type": "integer", 101 | "minimum": 0, 102 | "default": 0 103 | }, 104 | 105 | "maxLength": { 106 | "type": "integer" 107 | }, 108 | 109 | "enum": { 110 | "type": "array", 111 | "minItems": 1, 112 | "uniqueItems": true 113 | }, 114 | 115 | "default": { 116 | "type": "any" 117 | }, 118 | 119 | "title": { 120 | "type": "string" 121 | }, 122 | 123 | "description": { 124 | "type": "string" 125 | }, 126 | 127 | "format": { 128 | "type": "string" 129 | }, 130 | 131 | "divisibleBy": { 132 | "type": "number", 133 | "minimum": 0, 134 | "exclusiveMinimum": true, 135 | "default": 1 136 | }, 137 | 138 | "disallow": { 139 | "type": [ "string", "array" ], 140 | "items": { 141 | "type": [ "string", { "$ref": "#" } ] 142 | }, 143 | "uniqueItems": true 144 | }, 145 | 146 | "extends": { 147 | "type": [ { "$ref": "#" }, "array" ], 148 | "items": { "$ref": "#" }, 149 | "default": {} 150 | }, 151 | 152 | "id": { 153 | "type": "string", 154 | "format": "uri" 155 | }, 156 | 157 | "$ref": { 158 | "type": "string", 159 | "format": "uri" 160 | }, 161 | 162 | "$schema": { 163 | "type": "string", 164 | "format": "uri" 165 | } 166 | }, 167 | 168 | "dependencies": { 169 | "exclusiveMinimum": "minimum", 170 | "exclusiveMaximum": "maximum" 171 | }, 172 | 173 | "default": {} 174 | } 175 | -------------------------------------------------------------------------------- /test/support/string_validation.rb: -------------------------------------------------------------------------------- 1 | module StringValidation 2 | module ValueTests 3 | def test_minlength 4 | schema = { 5 | 'properties' => { 6 | 'a' => { 'minLength' => 1 } 7 | } 8 | } 9 | 10 | assert_valid schema, {'a' => 't'} 11 | refute_valid schema, {'a' => ''} 12 | 13 | # other types are disregarded 14 | assert_valid schema, {'a' => 5} 15 | end 16 | 17 | def test_maxlength 18 | schema = { 19 | 'properties' => { 20 | 'a' => { 'maxLength' => 2 } 21 | } 22 | } 23 | 24 | assert_valid schema, {'a' => 'tt'} 25 | assert_valid schema, {'a' => ''} 26 | refute_valid schema, {'a' => 'ttt'} 27 | 28 | # other types are disregarded 29 | assert_valid schema, {'a' => 5} 30 | end 31 | 32 | def test_pattern 33 | schema = { 34 | 'properties' => { 35 | 'a' => { 'pattern' => "\\d+ taco" } 36 | } 37 | } 38 | 39 | assert_valid schema, {'a' => '156 taco bell'} 40 | refute_valid schema, {'a' => 'x taco'} 41 | 42 | # other types are disregarded 43 | assert_valid schema, {'a' => 5} 44 | end 45 | end 46 | 47 | module FormatTests 48 | # Draft1..3 use the format name `ip-address`; draft4 changed it to `ipv4`. 49 | def ipv4_format 50 | 'ip-address' 51 | end 52 | 53 | def test_format_unknown 54 | schema = { 55 | 'properties' => { 56 | 'a' => { 'format' => 'unknown' } 57 | } 58 | } 59 | 60 | assert_valid schema, {'a' => 'absolutely anything!'} 61 | assert_valid schema, {'a' => ''} 62 | end 63 | 64 | def test_format_union 65 | schema = { 66 | 'properties' => { 67 | 'a' => { 68 | 'type' => ['string', 'null'], 69 | 'format' => 'date-time' 70 | } 71 | } 72 | } 73 | 74 | assert_valid schema, {'a' => nil} 75 | refute_valid schema, {'a' => 'wrong'} 76 | end 77 | 78 | def test_format_ipv4 79 | schema = { 80 | 'properties' => { 81 | 'a' => { 'format' => ipv4_format } 82 | } 83 | } 84 | 85 | assert_valid schema, {"a" => "1.1.1.1"} 86 | refute_valid schema, {"a" => "1.1.1"} 87 | refute_valid schema, {"a" => "1.1.1.300"} 88 | refute_valid schema, {"a" => "1.1.1"} 89 | refute_valid schema, {"a" => "1.1.1.1b"} 90 | 91 | # other types are disregarded 92 | assert_valid schema, {'a' => 5} 93 | end 94 | 95 | def test_format_ipv6 96 | schema = { 97 | 'properties' => { 98 | 'a' => { 'format' => 'ipv6' } 99 | } 100 | } 101 | 102 | assert_valid schema, {"a" => "1111:2222:8888:9999:aaaa:cccc:eeee:ffff"} 103 | assert_valid schema, {"a" => "1111:0:8888:0:0:0:eeee:ffff"} 104 | assert_valid schema, {"a" => "1111:2222:8888::eeee:ffff"} 105 | assert_valid schema, {"a" => "::1"} 106 | 107 | refute_valid schema, {"a" => "1111:2222:8888:99999:aaaa:cccc:eeee:ffff"} 108 | refute_valid schema, {"a" => "1111:2222:8888:9999:aaaa:cccc:eeee:gggg"} 109 | refute_valid schema, {"a" => "1111:2222::9999::cccc:eeee:ffff"} 110 | refute_valid schema, {"a" => "1111:2222:8888:9999:aaaa:cccc:eeee:ffff:bbbb"} 111 | refute_valid schema, {"a" => "42"} 112 | refute_valid schema, {"a" => "b"} 113 | end 114 | end 115 | 116 | # Draft1..3 explicitly support `date`, `time` formats in addition to 117 | # the `date-time` format. 118 | module DateAndTimeFormatTests 119 | def test_format_time 120 | schema = { 121 | 'properties' => { 122 | 'a' => { 'format' => 'time' } 123 | } 124 | } 125 | 126 | assert_valid schema, {"a" => "12:00:00"} 127 | refute_valid schema, {"a" => "12:00"} 128 | refute_valid schema, {"a" => "12:00:60"} 129 | refute_valid schema, {"a" => "12:60:00"} 130 | refute_valid schema, {"a" => "24:00:00"} 131 | refute_valid schema, {"a" => "0:00:00"} 132 | refute_valid schema, {"a" => "-12:00:00"} 133 | refute_valid schema, {"a" => "12:00:00b"} 134 | assert_valid schema, {"a" => "12:00:00"} 135 | refute_valid schema, {"a" => "12:00:00\nabc"} 136 | end 137 | 138 | def test_format_date 139 | schema = { 140 | 'properties' => { 141 | 'a' => { 'format' => 'date' } 142 | } 143 | } 144 | 145 | assert_valid schema, {"a" => "2010-01-01"} 146 | refute_valid schema, {"a" => "2010-01-32"} 147 | refute_valid schema, {"a" => "n2010-01-01"} 148 | refute_valid schema, {"a" => "2010-1-01"} 149 | refute_valid schema, {"a" => "2010-01-1"} 150 | refute_valid schema, {"a" => "2010-01-01n"} 151 | refute_valid schema, {"a" => "2010-01-01\nabc"} 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /resources/draft-06.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft/schema#", 3 | "$id": "http://json-schema.org/draft/schema#", 4 | "title": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ 17 | { "$ref": "#/definitions/positiveInteger" }, 18 | { "default": 0 } 19 | ] 20 | }, 21 | "simpleTypes": { 22 | "enum": [ 23 | "array", 24 | "boolean", 25 | "integer", 26 | "null", 27 | "number", 28 | "object", 29 | "string" 30 | ] 31 | }, 32 | "stringArray": { 33 | "type": "array", 34 | "items": { "type": "string" }, 35 | "uniqueItems": true, 36 | "defaultItems": [] 37 | } 38 | }, 39 | "type": ["object", "boolean"], 40 | "properties": { 41 | "$id": { 42 | "type": "string", 43 | "format": "uri-reference" 44 | }, 45 | "$schema": { 46 | "type": "string", 47 | "format": "uri" 48 | }, 49 | "$ref": { 50 | "type": "string", 51 | "format": "uri-reference" 52 | }, 53 | "title": { 54 | "type": "string" 55 | }, 56 | "description": { 57 | "type": "string" 58 | }, 59 | "default": {}, 60 | "multipleOf": { 61 | "type": "number", 62 | "exclusiveMinimum": 0 63 | }, 64 | "maximum": { 65 | "type": "number" 66 | }, 67 | "exclusiveMaximum": { 68 | "type": "number" 69 | }, 70 | "minimum": { 71 | "type": "number" 72 | }, 73 | "exclusiveMinimum": { 74 | "type": "number" 75 | }, 76 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 77 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 78 | "pattern": { 79 | "type": "string", 80 | "format": "regex" 81 | }, 82 | "additionalItems": { "$ref": "#" }, 83 | "items": { 84 | "anyOf": [ 85 | { "$ref": "#" }, 86 | { "$ref": "#/definitions/schemaArray" } 87 | ], 88 | "default": {} 89 | }, 90 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 91 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "uniqueItems": { 93 | "type": "boolean", 94 | "default": false 95 | }, 96 | "contains": { "$ref": "#" }, 97 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 98 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 99 | "required": { "$ref": "#/definitions/stringArray" }, 100 | "additionalProperties": { "$ref": "#" }, 101 | "definitions": { 102 | "type": "object", 103 | "additionalProperties": { "$ref": "#" }, 104 | "default": {} 105 | }, 106 | "properties": { 107 | "type": "object", 108 | "additionalProperties": { "$ref": "#" }, 109 | "default": {} 110 | }, 111 | "patternProperties": { 112 | "type": "object", 113 | "additionalProperties": { "$ref": "#" }, 114 | "default": {} 115 | }, 116 | "dependencies": { 117 | "type": "object", 118 | "additionalProperties": { 119 | "anyOf": [ 120 | { "$ref": "#" }, 121 | { "$ref": "#/definitions/stringArray" } 122 | ] 123 | } 124 | }, 125 | "propertyNames": { "$ref": "#" }, 126 | "const": {}, 127 | "enum": { 128 | "type": "array", 129 | "minItems": 1, 130 | "uniqueItems": true 131 | }, 132 | "type": { 133 | "anyOf": [ 134 | { "$ref": "#/definitions/simpleTypes" }, 135 | { 136 | "type": "array", 137 | "items": { "$ref": "#/definitions/simpleTypes" }, 138 | "minItems": 1, 139 | "uniqueItems": true 140 | } 141 | ] 142 | }, 143 | "format": { "type": "string" }, 144 | "allOf": { "$ref": "#/definitions/schemaArray" }, 145 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 146 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 147 | "not": { "$ref": "#" } 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /resources/draft-04.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /lib/json-schema/schema/reader.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'pathname' 3 | 4 | module JSON 5 | class Schema 6 | # Base for any reading exceptions encountered by {JSON::Schema::Reader} 7 | class ReadError < StandardError 8 | # @return [String] the requested schema location which was refused 9 | attr_reader :location 10 | 11 | # @return [Symbol] either +:uri+ or +:file+ 12 | attr_reader :type 13 | 14 | def initialize(location, type) 15 | @location = location 16 | @type = type 17 | super(error_message) 18 | end 19 | 20 | private 21 | 22 | def type_string 23 | type == :uri ? 'URI' : type.to_s 24 | end 25 | end 26 | 27 | # Raised by {JSON::Schema::Reader} when one of its settings indicate 28 | # a schema should not be read. 29 | class ReadRefused < ReadError 30 | private 31 | def error_message 32 | "Read of #{type_string} at #{location} refused" 33 | end 34 | end 35 | 36 | # Raised by {JSON::Schema::Reader} when an attempt to read a schema fails 37 | class ReadFailed < ReadError 38 | private 39 | def error_message 40 | "Read of #{type_string} at #{location} failed" 41 | end 42 | end 43 | 44 | # When an unregistered schema is encountered, the {JSON::Schema::Reader} is 45 | # used to fetch its contents and register it with the {JSON::Validator}. 46 | # 47 | # This default reader will read schemas from the filesystem or from a URI. 48 | class Reader 49 | # The behavior of the schema reader can be controlled by providing 50 | # callbacks to determine whether to permit reading referenced schemas. 51 | # The options +accept_uri+ and +accept_file+ should be procs which 52 | # accept a +URI+ or +Pathname+ object, and return a boolean value 53 | # indicating whether to read the referenced schema. 54 | # 55 | # URIs using the +file+ scheme will be normalized into +Pathname+ objects 56 | # and passed to the +accept_file+ callback. 57 | # 58 | # @param options [Hash] 59 | # @option options [Boolean, #call] accept_uri (true) 60 | # @option options [Boolean, #call] accept_file (true) 61 | # 62 | # @example Reject all unregistered schemas 63 | # JSON::Validator.schema_reader = JSON::Schema::Reader.new( 64 | # :accept_uri => false, 65 | # :accept_file => false 66 | # ) 67 | # 68 | # @example Only permit URIs from certain hosts 69 | # JSON::Validator.schema_reader = JSON::Schema::Reader.new( 70 | # :accept_file => false, 71 | # :accept_uri => proc { |uri| ['mycompany.com', 'json-schema.org'].include?(uri.host) } 72 | # ) 73 | def initialize(options = {}) 74 | @accept_uri = options.fetch(:accept_uri, true) 75 | @accept_file = options.fetch(:accept_file, true) 76 | end 77 | 78 | # @param location [#to_s] The location from which to read the schema 79 | # @return [JSON::Schema] 80 | # @raise [JSON::Schema::ReadRefused] if +accept_uri+ or +accept_file+ 81 | # indicated the schema could not be read 82 | # @raise [JSON::Schema::ParseError] if the schema was not a valid JSON object 83 | # @raise [JSON::Schema::ReadFailed] if reading the location was acceptable but the 84 | # attempt to retrieve it failed 85 | def read(location) 86 | uri = JSON::Util::URI.parse(location.to_s) 87 | body = if uri.scheme.nil? || uri.scheme == 'file' 88 | uri = JSON::Util::URI.file_uri(uri) 89 | read_file(Pathname.new(uri.path).expand_path) 90 | else 91 | read_uri(uri) 92 | end 93 | 94 | JSON::Schema.new(JSON::Validator.parse(body), uri) 95 | end 96 | 97 | # @param uri [Addressable::URI] 98 | # @return [Boolean] 99 | def accept_uri?(uri) 100 | if @accept_uri.respond_to?(:call) 101 | @accept_uri.call(uri) 102 | else 103 | @accept_uri 104 | end 105 | end 106 | 107 | # @param pathname [Pathname] 108 | # @return [Boolean] 109 | def accept_file?(pathname) 110 | if @accept_file.respond_to?(:call) 111 | @accept_file.call(pathname) 112 | else 113 | @accept_file 114 | end 115 | end 116 | 117 | private 118 | 119 | def read_uri(uri) 120 | if accept_uri?(uri) 121 | open(uri.to_s).read 122 | else 123 | raise JSON::Schema::ReadRefused.new(uri.to_s, :uri) 124 | end 125 | rescue OpenURI::HTTPError, SocketError 126 | raise JSON::Schema::ReadFailed.new(uri.to_s, :uri) 127 | end 128 | 129 | def read_file(pathname) 130 | if accept_file?(pathname) 131 | File.read(JSON::Util::URI.unescaped_path(pathname.to_s)) 132 | else 133 | raise JSON::Schema::ReadRefused.new(pathname.to_s, :file) 134 | end 135 | rescue Errno::ENOENT 136 | raise JSON::Schema::ReadFailed.new(pathname.to_s, :file) 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/initialize_data_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class InitializeDataTest < Minitest::Test 4 | 5 | def test_parse_character_string 6 | schema = {'type' => 'string'} 7 | data = 'hello world' 8 | 9 | assert(JSON::Validator.validate(schema, data)) 10 | 11 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 12 | 13 | assert_raises(JSON::Schema::JsonParseError) do 14 | JSON::Validator.validate(schema, data, :json => true) 15 | end 16 | 17 | assert_raises(JSON::Schema::JsonLoadError) { JSON::Validator.validate(schema, data, :uri => true) } 18 | end 19 | 20 | def test_parse_integer_string 21 | schema = {'type' => 'integer'} 22 | data = '42' 23 | 24 | assert(JSON::Validator.validate(schema, data)) 25 | 26 | refute(JSON::Validator.validate(schema, data, :parse_data => false)) 27 | 28 | assert(JSON::Validator.validate(schema, data, :json => true)) 29 | 30 | assert_raises(JSON::Schema::JsonLoadError) { JSON::Validator.validate(schema, data, :uri => true) } 31 | end 32 | 33 | def test_parse_hash_string 34 | schema = { 'type' => 'object', 'properties' => { 'a' => { 'type' => 'string' } } } 35 | data = '{"a": "b"}' 36 | 37 | assert(JSON::Validator.validate(schema, data)) 38 | 39 | refute(JSON::Validator.validate(schema, data, :parse_data => false)) 40 | 41 | assert(JSON::Validator.validate(schema, data, :json => true)) 42 | 43 | assert_raises(JSON::Schema::UriError) { JSON::Validator.validate(schema, data, :uri => true) } 44 | end 45 | 46 | def test_parse_json_string 47 | schema = {'type' => 'string'} 48 | data = '"hello world"' 49 | 50 | assert(JSON::Validator.validate(schema, data)) 51 | 52 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 53 | 54 | assert(JSON::Validator.validate(schema, data, :json => true)) 55 | 56 | assert_raises(JSON::Schema::JsonLoadError) { JSON::Validator.validate(schema, data, :uri => true) } 57 | end 58 | 59 | def test_parse_plain_text_string 60 | schema = {'type' => 'string'} 61 | data = 'kapow' 62 | 63 | assert(JSON::Validator.validate(schema, data)) 64 | 65 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 66 | 67 | assert_raises(JSON::Schema::JsonParseError) do 68 | JSON::Validator.validate(schema, data, :json => true) 69 | end 70 | 71 | assert_raises(JSON::Schema::JsonLoadError) { JSON::Validator.validate(schema, data, :uri => true) } 72 | end 73 | 74 | def test_parse_valid_uri_string 75 | schema = {'type' => 'string'} 76 | data = 'http://foo.bar/' 77 | 78 | stub_request(:get, "foo.bar").to_return(:body => '"hello world"', :status => 200) 79 | 80 | assert(JSON::Validator.validate(schema, data)) 81 | 82 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 83 | 84 | assert_raises(JSON::Schema::JsonParseError) do 85 | JSON::Validator.validate(schema, data, :json => true) 86 | end 87 | 88 | assert(JSON::Validator.validate(schema, data, :uri => true)) 89 | end 90 | 91 | def test_parse_invalid_uri_string 92 | schema = {'type' => 'string'} 93 | data = 'http://foo.bar/' 94 | 95 | stub_request(:get, "foo.bar").to_timeout 96 | 97 | assert(JSON::Validator.validate(schema, data)) 98 | 99 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 100 | 101 | stub_request(:get, "foo.bar").to_return(:status => [500, "Internal Server Error"]) 102 | 103 | assert(JSON::Validator.validate(schema, data)) 104 | 105 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 106 | 107 | assert_raises(JSON::Schema::JsonParseError) do 108 | JSON::Validator.validate(schema, data, :json => true) 109 | end 110 | 111 | assert_raises(JSON::Schema::JsonLoadError) { JSON::Validator.validate(schema, data, :uri => true) } 112 | end 113 | 114 | def test_parse_invalid_scheme_string 115 | schema = {'type' => 'string'} 116 | data = 'pick one: [1, 2, 3]' 117 | 118 | assert(JSON::Validator.validate(schema, data)) 119 | 120 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 121 | 122 | assert_raises(JSON::Schema::JsonParseError) do 123 | JSON::Validator.validate(schema, data, :json => true) 124 | end 125 | 126 | assert_raises(JSON::Schema::UriError) { JSON::Validator.validate(schema, data, :uri => true) } 127 | end 128 | 129 | def test_parse_integer 130 | schema = {'type' => 'integer'} 131 | data = 42 132 | 133 | assert(JSON::Validator.validate(schema, data)) 134 | 135 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 136 | 137 | assert_raises(TypeError) { JSON::Validator.validate(schema, data, :json => true) } 138 | 139 | assert_raises(TypeError) { JSON::Validator.validate(schema, data, :uri => true) } 140 | end 141 | 142 | def test_parse_hash 143 | schema = { 'type' => 'object', 'properties' => { 'a' => { 'type' => 'string' } } } 144 | data = { 'a' => 'b' } 145 | 146 | assert(JSON::Validator.validate(schema, data)) 147 | 148 | assert(JSON::Validator.validate(schema, data, :parse_data => false)) 149 | 150 | assert_raises(TypeError) { JSON::Validator.validate(schema, data, :json => true) } 151 | 152 | assert_raises(TypeError) { JSON::Validator.validate(schema, data, :uri => true) } 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /test/schema_validation_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | require 'tmpdir' 3 | 4 | class SchemaValidationTest < Minitest::Test 5 | def valid_schema_v3 6 | { 7 | "$schema" => "http://json-schema.org/draft-03/schema#", 8 | "type" => "object", 9 | "properties" => { 10 | "b" => { 11 | "required" => true 12 | } 13 | } 14 | } 15 | end 16 | 17 | def invalid_schema_v3 18 | { 19 | "$schema" => "http://json-schema.org/draft-03/schema#", 20 | "type" => "object", 21 | "properties" => { 22 | "b" => { 23 | "required" => "true" 24 | } 25 | } 26 | } 27 | end 28 | 29 | def valid_schema_v4 30 | { 31 | "$schema" => "http://json-schema.org/draft-04/schema#", 32 | "type" => "object", 33 | "required" => ["b"], 34 | "properties" => { 35 | } 36 | } 37 | end 38 | 39 | def invalid_schema_v4 40 | { 41 | "$schema" => "http://json-schema.org/draft-04/schema#", 42 | "type" => "object", 43 | "required" => "b", 44 | "properties" => { 45 | } 46 | } 47 | end 48 | 49 | def symbolized_schema 50 | { 51 | :type => :object, 52 | :required => [ 53 | :id, 54 | :name, 55 | :real_name, 56 | :role, 57 | :website, 58 | :biography, 59 | :created_at, 60 | :demographic 61 | ], 62 | :properties => { 63 | :id => { 64 | :type => [ 65 | :integer 66 | ] 67 | }, 68 | :name => { 69 | :type => [ 70 | :string 71 | ] 72 | }, 73 | :real_name => { 74 | :type => [ 75 | :string 76 | ] 77 | }, 78 | :role => { 79 | :type => [ 80 | :string 81 | ] 82 | }, 83 | :website => { 84 | :type => [ 85 | :string, 86 | :null 87 | ] 88 | }, 89 | :created_at => { 90 | :type => [ 91 | :string 92 | ] 93 | }, 94 | :biography => { 95 | :type => [ 96 | :string, 97 | :null 98 | ] 99 | } 100 | }, 101 | :relationships => { 102 | :demographic => { 103 | :type => :object, 104 | :required => [ 105 | :id, 106 | :gender 107 | ], 108 | :properties => { 109 | :id => { 110 | :type => [ 111 | :integer 112 | ] 113 | }, 114 | :gender => { 115 | :type => [ 116 | :string 117 | ] 118 | } 119 | } 120 | } 121 | } 122 | } 123 | end 124 | 125 | def test_draft03_validation 126 | data = {"b" => {"a" => 5}} 127 | assert(JSON::Validator.validate(valid_schema_v3,data,:validate_schema => true, :version => :draft3)) 128 | assert(!JSON::Validator.validate(invalid_schema_v3,data,:validate_schema => true, :version => :draft3)) 129 | end 130 | 131 | def test_validate_just_schema_draft03 132 | errors = JSON::Validator.fully_validate_schema(valid_schema_v3, :version => :draft3) 133 | assert_equal [], errors 134 | 135 | errors = JSON::Validator.fully_validate_schema(invalid_schema_v3, :version => :draft3) 136 | assert_equal 1, errors.size 137 | assert_match(/the property .*required.*did not match/i, errors.first) 138 | end 139 | 140 | 141 | def test_draft04_validation 142 | data = {"b" => {"a" => 5}} 143 | assert(JSON::Validator.validate(valid_schema_v4,data,:validate_schema => true, :version => :draft4)) 144 | assert(!JSON::Validator.validate(invalid_schema_v4,data,:validate_schema => true, :version => :draft4)) 145 | end 146 | 147 | def test_validate_just_schema_draft04 148 | errors = JSON::Validator.fully_validate_schema(valid_schema_v4, :version => :draft4) 149 | assert_equal [], errors 150 | 151 | errors = JSON::Validator.fully_validate_schema(invalid_schema_v4, :version => :draft4) 152 | assert_equal 1, errors.size 153 | assert_match(/the property .*required.*did not match/i, errors.first) 154 | end 155 | 156 | def test_validate_schema_3_without_version_option 157 | data = {"b" => {"a" => 5}} 158 | assert(JSON::Validator.validate(valid_schema_v3,data,:validate_schema => true)) 159 | assert(!JSON::Validator.validate(invalid_schema_v3,data,:validate_schema => true)) 160 | end 161 | 162 | def test_schema_validation_from_different_directory 163 | Dir.mktmpdir do |tmpdir| 164 | Dir.chdir(tmpdir) do 165 | data = {"b" => {"a" => 5}} 166 | assert(JSON::Validator.validate(valid_schema_v4,data,:validate_schema => true, :version => :draft4)) 167 | assert(!JSON::Validator.validate(invalid_schema_v4,data,:validate_schema => true, :version => :draft4)) 168 | end 169 | end 170 | end 171 | 172 | def test_validate_schema_with_symbol_keys 173 | data = { 174 | "created_at" => "2014-01-25T00:58:33-08:00", 175 | "id" => 8517194300913402149003, 176 | "name" => "chelsey", 177 | "real_name" => "Mekhi Hegmann", 178 | "website" => nil, 179 | "role" => "user", 180 | "biography" => nil, 181 | "demographic" => nil 182 | } 183 | assert(JSON::Validator.validate!(symbolized_schema, data, :validate_schema => true)) 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/full_validation_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../support/test_helper', __FILE__) 2 | 3 | class FullValidationTest < Minitest::Test 4 | 5 | def test_full_validation 6 | data = {"b" => {"a" => 5}} 7 | schema = { 8 | "type" => "object", 9 | "required" => ["b"], 10 | "properties" => { 11 | "b" => { 12 | } 13 | } 14 | } 15 | 16 | errors = JSON::Validator.fully_validate(schema,data) 17 | assert(errors.empty?) 18 | 19 | data = {"c" => 5} 20 | schema = { 21 | "type" => "object", 22 | "required" => ["b"], 23 | "properties" => { 24 | "b" => { 25 | }, 26 | "c" => { 27 | "type" => "string" 28 | } 29 | } 30 | } 31 | 32 | errors = JSON::Validator.fully_validate(schema,data) 33 | assert(errors.length == 2) 34 | end 35 | 36 | def test_full_validation_with_union_types 37 | data = {"b" => 5} 38 | schema = { 39 | "type" => "object", 40 | "properties" => { 41 | "b" => { 42 | "type" => ["null","integer"] 43 | } 44 | } 45 | } 46 | 47 | errors = JSON::Validator.fully_validate(schema,data) 48 | assert(errors.empty?) 49 | 50 | schema = { 51 | "type" => "object", 52 | "properties" => { 53 | "b" => { 54 | "type" => ["integer","null"] 55 | } 56 | } 57 | } 58 | 59 | errors = JSON::Validator.fully_validate(schema,data) 60 | assert(errors.empty?) 61 | 62 | data = {"b" => "a string"} 63 | 64 | errors = JSON::Validator.fully_validate(schema,data) 65 | assert(errors.length == 1) 66 | 67 | schema = { 68 | "$schema" => "http://json-schema.org/draft-03/schema#", 69 | "type" => "object", 70 | "properties" => { 71 | "b" => { 72 | "type" => [ 73 | { 74 | "type" => "object", 75 | "properties" => { 76 | "c" => {"type" => "string"} 77 | } 78 | }, 79 | { 80 | "type" => "object", 81 | "properties" => { 82 | "d" => {"type" => "integer"} 83 | } 84 | } 85 | ] 86 | } 87 | } 88 | } 89 | 90 | data = {"b" => {"c" => "taco"}} 91 | 92 | errors = JSON::Validator.fully_validate(schema,data) 93 | assert(errors.empty?) 94 | 95 | data = {"b" => {"d" => 6}} 96 | 97 | errors = JSON::Validator.fully_validate(schema,data) 98 | assert(errors.empty?) 99 | 100 | data = {"b" => {"c" => 6, "d" => "OH GOD"}} 101 | 102 | errors = JSON::Validator.fully_validate(schema,data) 103 | assert(errors.length == 1) 104 | end 105 | 106 | 107 | def test_full_validation_with_object_errors 108 | data = {"b" => {"a" => 5}} 109 | schema = { 110 | "type" => "object", 111 | "required" => ["b"], 112 | "properties" => { 113 | "b" => { 114 | } 115 | } 116 | } 117 | 118 | errors = JSON::Validator.fully_validate(schema,data,:errors_as_objects => true) 119 | assert(errors.empty?) 120 | 121 | data = {"c" => 5} 122 | schema = { 123 | "type" => "object", 124 | "required" => ["b"], 125 | "properties" => { 126 | "b" => { 127 | }, 128 | "c" => { 129 | "type" => "string" 130 | } 131 | } 132 | } 133 | 134 | errors = JSON::Validator.fully_validate(schema,data,:errors_as_objects => true) 135 | 136 | assert(errors.length == 2) 137 | assert(errors[0][:failed_attribute] == "Required") 138 | assert(errors[0][:fragment] == "#/") 139 | assert(errors[1][:failed_attribute] == "TypeV4") 140 | assert(errors[1][:fragment] == "#/c") 141 | end 142 | 143 | def test_full_validation_with_nested_required_properties 144 | schema = { 145 | "type" => "object", 146 | "required" => ["x"], 147 | "properties" => { 148 | "x" => { 149 | "type" => "object", 150 | "required" => ["a", "b"], 151 | "properties" => { 152 | "a" => {"type"=>"integer"}, 153 | "b" => {"type"=>"integer"}, 154 | "c" => {"type"=>"integer"}, 155 | "d" => {"type"=>"integer"}, 156 | "e" => {"type"=>"integer"}, 157 | } 158 | } 159 | } 160 | } 161 | data = {"x" => {"a"=>5, "d"=>5, "e"=>"what?"}} 162 | 163 | errors = JSON::Validator.fully_validate(schema,data,:errors_as_objects => true) 164 | assert_equal 2, errors.length 165 | assert_equal '#/x', errors[0][:fragment] 166 | assert_equal 'Required', errors[0][:failed_attribute] 167 | assert_equal '#/x/e', errors[1][:fragment] 168 | assert_equal 'TypeV4', errors[1][:failed_attribute] 169 | end 170 | 171 | def test_full_validation_with_nested_required_propertiesin_array 172 | schema = { 173 | "type" => "object", 174 | "required" => ["x"], 175 | "properties" => { 176 | "x" => { 177 | "type" => "array", 178 | "items" => { 179 | "type" => "object", 180 | "required" => ["a", "b"], 181 | "properties" => { 182 | "a" => {"type"=>"integer"}, 183 | "b" => {"type"=>"integer"}, 184 | "c" => {"type"=>"integer"}, 185 | "d" => {"type"=>"integer"}, 186 | "e" => {"type"=>"integer"}, 187 | } 188 | } 189 | } 190 | } 191 | } 192 | missing_b= {"a"=>5} 193 | e_is_wrong_type= {"a"=>5,"b"=>5,"e"=>"what?"} 194 | data = {"x" => [missing_b, e_is_wrong_type]} 195 | 196 | errors = JSON::Validator.fully_validate(schema,data,:errors_as_objects => true) 197 | assert_equal 2, errors.length 198 | assert_equal '#/x/0', errors[0][:fragment] 199 | assert_equal 'Required', errors[0][:failed_attribute] 200 | assert_equal '#/x/1/e', errors[1][:fragment] 201 | assert_equal 'TypeV4', errors[1][:failed_attribute] 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /test/custom_format_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.expand_path('../support/test_helper', __FILE__) 3 | 4 | class CustomFormatTest < Minitest::Test 5 | def setup 6 | @all_versions = ['draft1', 'draft2', 'draft3', 'draft4', 'draft6', nil] 7 | @format_proc = lambda { |value| raise JSON::Schema::CustomFormatError.new("must be 42") unless value == "42" } 8 | @schema_6 = { 9 | "$schema" => "http://json-schema.org/draft/schema#", 10 | "properties" => { 11 | "a" => { 12 | "type" => "string", 13 | "format" => "custom", 14 | }, 15 | } 16 | } 17 | @schema_4 = @schema_6.clone 18 | @schema_4["$schema"] = "http://json-schema.org/draft-04/schema#" 19 | @schema_3 = @schema_6.clone 20 | @schema_3["$schema"] = "http://json-schema.org/draft-03/schema#" 21 | @schema_2 = @schema_6.clone 22 | @schema_2["$schema"] = "http://json-schema.org/draft-02/schema#" 23 | @schema_1 = @schema_6.clone 24 | @schema_1["$schema"] = "http://json-schema.org/draft-01/schema#" 25 | @default = @schema_6.clone 26 | @default.delete("$schema") 27 | @schemas = { 28 | "draft1" => @schema_1, 29 | "draft2" => @schema_2, 30 | "draft3" => @schema_3, 31 | "draft4" => @schema_4, 32 | "draft6" => @schema_6, 33 | nil => @default, 34 | } 35 | JSON::Validator.restore_default_formats 36 | end 37 | 38 | def test_single_registration 39 | @all_versions.each do |version| 40 | assert(JSON::Validator.validator_for_name(version).formats['custom'].nil?, "Format 'custom' for #{version || 'default'} should be nil") 41 | JSON::Validator.register_format_validator("custom", @format_proc, [version]) 42 | assert(JSON::Validator.validator_for_name(version).formats['custom'].is_a?(JSON::Schema::CustomFormat), "Format 'custom' should be registered for #{version || 'default'}") 43 | (@all_versions - [version]).each do |other_version| 44 | assert(JSON::Validator.validator_for_name(other_version).formats['custom'].nil?, "Format 'custom' should still be nil for #{other_version || 'default'}") 45 | end 46 | JSON::Validator.deregister_format_validator("custom", [version]) 47 | assert(JSON::Validator.validator_for_name(version).formats['custom'].nil?, "Format 'custom' should be deregistered for #{version || 'default'}") 48 | end 49 | end 50 | 51 | def test_register_for_all_by_default 52 | JSON::Validator.register_format_validator("custom", @format_proc) 53 | @all_versions.each do |version| 54 | assert(JSON::Validator.validator_for_name(version).formats['custom'].is_a?(JSON::Schema::CustomFormat), "Format 'custom' should be registered for #{version || 'default'}") 55 | end 56 | JSON::Validator.restore_default_formats 57 | @all_versions.each do |version| 58 | assert(JSON::Validator.validator_for_name(version).formats['custom'].nil?, "Format 'custom' should still be nil for #{version || 'default'}") 59 | end 60 | end 61 | 62 | def test_multi_registration 63 | unregistered_version = @all_versions.delete("draft1") 64 | JSON::Validator.register_format_validator("custom", @format_proc, @all_versions) 65 | @all_versions.each do |version| 66 | assert(JSON::Validator.validator_for_name(version).formats['custom'].is_a?(JSON::Schema::CustomFormat), "Format 'custom' should be registered for #{version || 'default'}") 67 | end 68 | assert(JSON::Validator.validator_for_name(unregistered_version).formats['custom'].nil?, "Format 'custom' should still be nil for #{unregistered_version}") 69 | end 70 | 71 | def test_format_validation 72 | @all_versions.each do |version| 73 | data = { 74 | "a" => "23" 75 | } 76 | schema = @schemas[version] 77 | prefix = "Validation for '#{version || 'default'}'" 78 | 79 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with no 'custom' format validator registered") 80 | 81 | JSON::Validator.register_format_validator("custom", @format_proc, [version]) 82 | data["a"] = "42" 83 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with 'custom' format validator and correct data") 84 | 85 | data["a"] = "23" 86 | assert(!JSON::Validator.validate(schema, data), "#{prefix} fails with 'custom' format validator and wrong data") 87 | 88 | errors = JSON::Validator.fully_validate(schema, data) 89 | assert_equal(errors.count, 1) 90 | assert_match(/The property '#\/a' must be 42 in schema/, errors.first, "#{prefix} records format error") 91 | 92 | data["a"] = 23 93 | errors = JSON::Validator.fully_validate(schema, data) 94 | assert_equal(errors.count, 1) 95 | assert_match(/The property '#\/a' of type integer did not match the following type: string/i, errors.first, "#{prefix} records no format error on type mismatch") 96 | end 97 | end 98 | 99 | def test_override_default_format 100 | @all_versions.each do |version| 101 | data = { 102 | "a" => "2001:db8:85a3:0:0:8a2e:370:7334" 103 | } 104 | schema = @schemas[version] 105 | schema["properties"]["a"]["format"] = "ipv6" 106 | prefix = "Validation for '#{version || 'default'}'" 107 | 108 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds for default format with correct data") 109 | 110 | data["a"] = "no_ip6_address" 111 | assert(!JSON::Validator.validate(schema, data), "#{prefix} fails for default format and wrong data") 112 | 113 | data["a"] = "42" 114 | JSON::Validator.register_format_validator("ipv6", @format_proc, [version]) 115 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with overriden default format and correct data") 116 | 117 | JSON::Validator.deregister_format_validator("ipv6", [version]) 118 | data["a"] = "2001:db8:85a3:0:0:8a2e:370:7334" 119 | assert(JSON::Validator.validate(schema, data), "#{prefix} restores the default format on deregistration") 120 | end 121 | end 122 | end 123 | --------------------------------------------------------------------------------