├── 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 │ ├── any_of_ref_jane_schema.json │ ├── any_of_ref_jimmy_schema.json │ ├── any_of_ref_john_schema.json │ ├── all_of_ref_base_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 ├── type_attribute_test.rb ├── list_option_test.rb ├── draft6_test.rb ├── caching_test.rb ├── no_additional_properties_all_of_test.rb ├── stringify_test.rb ├── ruby_schema_test.rb ├── support │ ├── test_helper.rb │ ├── object_validation.rb │ ├── number_validation.rb │ ├── type_validation.rb │ ├── array_validation.rb │ ├── enum_validation.rb │ ├── string_validation.rb │ └── strict_validation.rb ├── bad_schema_ref_test.rb ├── extends_nested_test.rb ├── load_ref_schema_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 ├── all_of_ref_schema_test.rb ├── common_test_suite_test.rb ├── files_test.rb ├── extended_schema_test.rb ├── schema_reader_test.rb ├── fragment_resolution_test.rb ├── draft2_test.rb ├── one_of_test.rb ├── draft1_test.rb ├── schema_validation_test.rb └── custom_format_test.rb ├── .rubocop.yml ├── .gitignore ├── 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 │ │ ├── disallow.rb │ │ ├── properties_v4.rb │ │ ├── format.rb │ │ ├── uniqueitems.rb │ │ ├── const.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 │ │ ├── propertynames.rb │ │ ├── type_v4.rb │ │ ├── properties_optional.rb │ │ ├── required.rb │ │ ├── additionalitems.rb │ │ ├── not.rb │ │ ├── limit.rb │ │ ├── dependencies.rb │ │ ├── extends.rb │ │ ├── anyof.rb │ │ ├── oneof.rb │ │ ├── additionalproperties.rb │ │ ├── ref.rb │ │ ├── properties.rb │ │ ├── allof.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 │ │ ├── draft4.rb │ │ └── draft6.rb │ ├── util │ │ ├── array_set.rb │ │ └── uri.rb │ ├── schema │ │ ├── validator.rb │ │ └── reader.rb │ ├── attribute.rb │ └── schema.rb └── json-schema.rb ├── .gitmodules ├── Gemfile ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── test.yml │ └── release.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── json-schema.gemspec ├── LICENSE.md ├── Rakefile └── 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 | } -------------------------------------------------------------------------------- /test/data/bad_data_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "a" : "poop" 3 | } -------------------------------------------------------------------------------- /test/data/all_of_ref_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "john", 3 | "id" : "1" 4 | } 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: .rubocop_todo.yml 3 | inherit_gem: 4 | voxpupuli-rubocop: rubocop.yml 5 | -------------------------------------------------------------------------------- /test/data/any_of_ref_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "names" : 3 | [ "john" 4 | , "jane" 5 | , "jimmy" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | .*.swp 3 | pkg 4 | *.gem 5 | /Gemfile.lock 6 | .bundle 7 | .idea 8 | /coverage 9 | /vendor 10 | /.vendor 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/schemas/all_of_ref_base_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties" : { 4 | "name" : { "type": "integer" }, 5 | "id" : { "type": "string" } 6 | } 7 | } 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 | } -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :release, optional: true do 6 | gem 'faraday-retry', '~> 2.1', require: false 7 | gem 'github_changelog_generator', '~> 1.16.4', require: false 8 | end 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft1.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class HyperDraft1 < Draft1 4 | def initialize 5 | super 6 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-01/hyper-schema#') 7 | end 8 | 9 | JSON::Validator.register_validator(new) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft2.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class HyperDraft2 < Draft2 4 | def initialize 5 | super 6 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-02/hyper-schema#') 7 | end 8 | 9 | JSON::Validator.register_validator(new) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft3.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class HyperDraft3 < Draft3 4 | def initialize 5 | super 6 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-03/hyper-schema#') 7 | end 8 | 9 | JSON::Validator.register_validator(new) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft4.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class HyperDraft4 < Draft4 4 | def initialize 5 | super 6 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-04/hyper-schema#') 7 | end 8 | 9 | JSON::Validator.register_validator(new) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/json-schema/validators/hyper-draft6.rb: -------------------------------------------------------------------------------- 1 | module JSON 2 | class Schema 3 | class HyperDraft6 < Draft6 4 | def initialize 5 | super 6 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-06/hyper-schema#') 7 | end 8 | 9 | JSON::Validator.register_validator(new) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /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 | 9 | type.validate(current_schema, data, fragments, processor, validator, options.merge(disallow: true)) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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[:allPropertiesRequired] == true 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/min_items_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 13 | assert_equal(1, errors.length) 14 | refute_match(/minimum/, errors[0]) 15 | assert_match(/null/, errors[0]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /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 | 9 | format = current_schema.schema['format'].to_s 10 | validator = validator.formats[format] 11 | validator&.validate(current_schema, data, fragments, processor, validator, options) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/json-schema/util/array_set.rb: -------------------------------------------------------------------------------- 1 | # This is a hack that I don't want to ever use anywhere else or repeat EVER, but we need enums to be 2 | # an Array to pass schema validation. But we also want fast lookup! 3 | 4 | class ArraySet < Array 5 | def include?(obj) 6 | unless defined? @values 7 | @values = Set.new 8 | each { |x| @values << convert_to_float_if_numeric(x) } 9 | end 10 | @values.include?(convert_to_float_if_numeric(obj)) 11 | end 12 | 13 | private 14 | 15 | def convert_to_float_if_numeric(value) 16 | value.is_a?(Numeric) ? value.to_f : value 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /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/attributes/const.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class ConstAttribute < Attribute 6 | def self.validate(current_schema, data, fragments, processor, _validator, options = {}) 7 | const_value = current_schema.schema['const'] 8 | unless const_value == data 9 | message = "The property '#{build_fragment(fragments)}' value #{data.inspect} did not match constant '#{const_value}'" 10 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 | 9 | DateTime.rfc3339(data) 10 | rescue ArgumentError 11 | error_message = "The property '#{build_fragment(fragments)}' must be a valid RFC3339 date/time string" 12 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/relative_definition_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class RelativeDefinitionTest < Minitest::Test 4 | def test_definition_schema 5 | assert_valid schema_fixture_path('definition_schema.json'), { 'a' => 5 } 6 | end 7 | 8 | def test_definition_schema_with_special_characters 9 | assert_valid schema_fixture_path('definition_schema_with_special_characters.json'), { 'a' => 5 } 10 | end 11 | 12 | def test_relative_definition 13 | schema = schema_fixture_path('relative_definition_schema.json') 14 | 15 | assert_valid schema, { 'a' => 5 } 16 | refute_valid schema, { 'a' => 'foo' } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/type_attribute_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class TypeAttributeTest < Minitest::Test 4 | def test_type_of_data 5 | assert_equal('string', type_of_data('')) 6 | assert_equal('number', type_of_data(Numeric.new)) 7 | assert_equal('integer', type_of_data(1)) 8 | assert_equal('boolean', type_of_data(true)) 9 | assert_equal('boolean', type_of_data(false)) 10 | assert_equal('object', type_of_data({})) 11 | assert_equal('null', type_of_data(nil)) 12 | assert_equal('any', type_of_data(Object.new)) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 10 | error_message = "The property '#{build_fragment(fragments)}' must be a valid URI" 11 | begin 12 | JSON::Util::URI.parse(data) 13 | rescue JSON::Schema::UriError 14 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /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/list_option_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 17 | assert_valid uri.to_s, data, clear_cache: false 18 | 19 | data = [{ 'a' => 1 }] 20 | 21 | assert_valid uri.to_s, data, list: true 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /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 | @validation_proc.call data 13 | rescue JSON::Schema::CustomFormatError => e 14 | message = "The property '#{self.class.build_fragment(fragments)}' #{e.message}" 15 | self.class.validation_error(processor, message, fragments, current_schema, self.class, options[:record_errors]) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /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 | 16 | schema = JSON::Schema.new(property_schema, current_schema.uri, validator) 17 | schema.validate(data[key], fragments + [key], processor, options) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /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 do |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 | end.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 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - duplicate 8 | - invalid 9 | - modulesync 10 | - question 11 | - skip-changelog 12 | - wont-fix 13 | - wontfix 14 | - github_actions 15 | 16 | categories: 17 | - title: Breaking Changes 🛠 18 | labels: 19 | - backwards-incompatible 20 | 21 | - title: New Features 🎉 22 | labels: 23 | - enhancement 24 | 25 | - title: Bug Fixes 🐛 26 | labels: 27 | - bug 28 | - bugfix 29 | 30 | - title: Documentation Updates 📚 31 | labels: 32 | - documentation 33 | - docs 34 | 35 | - title: Dependency Updates ⬆️ 36 | labels: 37 | - dependencies 38 | 39 | - title: Other Changes 40 | labels: 41 | - "*" 42 | -------------------------------------------------------------------------------- /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 | 21 | schema = JSON::Schema.new(item_schema, current_schema.uri, validator) 22 | schema.validate(data[i], fragments + [i.to_s], processor, options) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/json-schema/attributes/propertynames.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/attribute' 2 | 3 | module JSON 4 | class Schema 5 | class PropertyNames < Attribute 6 | def self.validate(current_schema, data, fragments, processor, validator, options = {}) 7 | return unless data.is_a?(Hash) 8 | 9 | propnames = current_schema.schema['propertyNames'] 10 | 11 | if propnames.is_a?(Hash) 12 | schema = JSON::Schema.new(propnames, current_schema.uri, validator) 13 | data.each_key do |key| 14 | schema.validate(key, fragments + [key], processor, options) 15 | end 16 | elsif propnames == false && data.any? 17 | message = "The property '#{build_fragment(fragments)}' contains additional properties #{data.keys.inspect} outside of the schema when none are allowed" 18 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /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 | 17 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 18 | end 19 | else 20 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | unless 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 | -------------------------------------------------------------------------------- /json-schema.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'json-schema' 3 | s.version = '6.1.0' 4 | s.authors = ['Kenny Hoxworth', 'Vox Pupuli'] 5 | s.email = 'voxpupuli@groups.io' 6 | s.homepage = 'https://github.com/voxpupuli/json-schema/' 7 | s.metadata = { 8 | 'source_code_uri' => s.homepage, 9 | 'changelog_uri' => "#{s.homepage}/blob/master/CHANGELOG.md", 10 | 'bug_tracker_uri' => "#{s.homepage}/issues", 11 | 'funding_uri' => 'https://github.com/sponsors/voxpupuli', 12 | } 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 = '>= 3.2' 18 | s.license = 'MIT' 19 | 20 | s.add_development_dependency 'minitest', '>= 5.0', '< 7' 21 | s.add_development_dependency 'rake', '~> 13.0' 22 | s.add_development_dependency 'voxpupuli-rubocop', '~> 5.1.0' 23 | s.add_development_dependency 'webmock', '~> 3.23' 24 | 25 | s.add_dependency 'addressable', '~> 2.8' 26 | s.add_dependency 'bigdecimal', '>= 3.1', '< 5' 27 | end 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/draft6_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class Draft6Test < Minitest::Test 4 | def validation_errors(schema, data, _options) 5 | super(schema, data, version: :draft6) 6 | end 7 | 8 | def test_const_attribute 9 | schema = { 10 | 'type' => 'object', 11 | 'properties' => { 12 | 'a' => { 'const' => 'foo' }, 13 | 'b' => { 'const' => 6 }, 14 | }, 15 | } 16 | 17 | data = { a: 'foo', b: 6 } 18 | 19 | assert_valid schema, data 20 | 21 | data = { a: 6, b: 'foo' } 22 | 23 | refute_valid schema, data 24 | end 25 | 26 | def test_property_names 27 | schema = { 28 | 'type' => 'object', 29 | 'propertyNames' => { 'const' => 'foo' }, 30 | } 31 | 32 | data = { 'foo' => 'value' } 33 | 34 | assert_valid schema, data 35 | 36 | data = { 'bar' => 'value' } 37 | 38 | refute_valid schema, data 39 | 40 | schema = { 41 | 'type' => 'object', 42 | 'propertyNames' => false, 43 | } 44 | 45 | data = {} 46 | 47 | assert_valid schema, data 48 | 49 | data = { 'foo' => 'value' } 50 | 51 | refute_valid schema, data 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 | 15 | prop_defaults = options[:insert_defaults] && 16 | defined_properties && 17 | defined_properties[property] && 18 | !defined_properties[property]['default'].nil? && 19 | !defined_properties[property]['readonly'] 20 | 21 | unless prop_defaults 22 | message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'" 23 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /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 | 23 | additional_items_schema.validate(item, fragments + [i.to_s], processor, options) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /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.start_with?('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 | -------------------------------------------------------------------------------- /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', __dir__) 33 | File.join(resources, @metaschema_name) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | pull_request: {} 6 | push: 7 | branches: 8 | - master 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - ruby: truffleruby 21 | - ruby: truffleruby+graalvm 22 | - ruby: jruby 23 | - ruby: "3.2" 24 | rake_task: "rubocop test" 25 | - ruby: "3.3" 26 | - ruby: "3.3" 27 | rubyopt: "--enable-frozen-string-literal" 28 | - ruby: "3.4" 29 | steps: 30 | - uses: actions/checkout@v6 31 | - name: Install Ruby ${{ matrix.ruby }} 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby }} 35 | bundler-cache: true 36 | - name: Run tests 37 | run: bundle exec rake ${{ matrix.rake_task }} RUBYOPT="${{ matrix.rubyopt }}" 38 | - name: Build gem 39 | run: gem build --strict --verbose *.gemspec 40 | 41 | tests: 42 | if: always() 43 | needs: 44 | - test 45 | runs-on: ubuntu-24.04 46 | name: Test suite 47 | steps: 48 | - name: Decide whether the needed jobs succeeded or failed 49 | uses: re-actors/alls-green@release/v1 50 | with: 51 | jobs: ${{ toJSON(needs) }} 52 | -------------------------------------------------------------------------------- /test/caching_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 18 | assert_valid(schema_path, 'foo', clear_cache: false) 19 | 20 | set_schema('type' => 'number') 21 | 22 | refute_valid(schema_path, 123) 23 | end 24 | 25 | def test_clear_cache 26 | set_schema('type' => 'string') 27 | 28 | assert_valid(schema_path, 'foo', clear_cache: true) 29 | 30 | set_schema('type' => 'number') 31 | 32 | assert_valid(schema_path, 123) 33 | end 34 | 35 | def test_cache_schemas 36 | suppress_warnings do 37 | JSON::Validator.cache_schemas = false 38 | end 39 | 40 | set_schema('type' => 'string') 41 | 42 | assert_valid(schema_path, 'foo', clear_cache: false) 43 | 44 | set_schema('type' => 'number') 45 | 46 | assert_valid(schema_path, 123) 47 | ensure 48 | suppress_warnings do 49 | JSON::Validator.cache_schemas = true 50 | end 51 | end 52 | 53 | private 54 | 55 | def schema_path 56 | @schema.path 57 | end 58 | 59 | def set_schema(schema_definition) 60 | @schema.write(schema_definition.to_json) 61 | @schema.rewind 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/no_additional_properties_all_of_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class NoAdditionalPropertiesAllOfTest < Minitest::Test 4 | def schema 5 | { 6 | '$schema': 'http://json-schema.org/draft-04/schema#', 7 | type: 'object', 8 | allOf: [ 9 | { 10 | type: 'object', 11 | properties: { 12 | a: { 13 | type: 'integer', 14 | }, 15 | }, 16 | required: ['a'], 17 | }, 18 | { 19 | type: 'object', 20 | properties: { 21 | b: { 22 | type: 'string', 23 | }, 24 | }, 25 | required: ['b'], 26 | }, 27 | ], 28 | } 29 | end 30 | 31 | def data 32 | { 33 | a: 1, 34 | b: 'hello', 35 | } 36 | end 37 | 38 | def test_all_of_ref_message 39 | assert_valid schema, data, { noAdditionalProperties: true } 40 | end 41 | 42 | def test_all_of_failures 43 | data = { 44 | a: 1, 45 | b: 'hello', 46 | c: 'something', 47 | } 48 | 49 | assert_raises JSON::Schema::ValidationError do 50 | JSON::Validator.validate!(schema, data, noAdditionalProperties: true) 51 | end 52 | 53 | data = { 54 | a: 1, 55 | b: 2, 56 | } 57 | 58 | assert_raises JSON::Schema::ValidationError do 59 | JSON::Validator.validate!(schema, data, noAdditionalProperties: true) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/stringify_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class StringifyTest < Minitest::Test 4 | def test_stringify_on_hash 5 | hash = { 6 | :a => 'foo', 7 | 'b' => :bar, 8 | } 9 | 10 | assert_equal({ 'a' => 'foo', 'b' => 'bar' }, JSON::Schema.stringify(hash), 'symbol keys should be converted to strings') 11 | end 12 | 13 | def test_stringify_on_array 14 | array = [ 15 | :a, 16 | 'b', 17 | ] 18 | 19 | assert_equal(%w[a b], JSON::Schema.stringify(array), 'symbols in an array should be converted to strings') 20 | end 21 | 22 | def test_stringify_on_hash_of_arrays 23 | hash = { 24 | :a => [:foo], 25 | 'b' => :bar, 26 | } 27 | 28 | assert_equal({ 'a' => ['foo'], 'b' => 'bar' }, JSON::Schema.stringify(hash), 'symbols in a nested array should be converted to strings') 29 | end 30 | 31 | def test_stringify_on_array_of_hashes 32 | array = [ 33 | :a, 34 | { 35 | b: :bar, 36 | }, 37 | ] 38 | 39 | assert_equal(['a', { 'b' => 'bar' }], JSON::Schema.stringify(array), 'symbols keys in a nested hash should be converted to strings') 40 | end 41 | 42 | def test_stringify_on_hash_of_hashes 43 | hash = { 44 | a: { 45 | b: { 46 | foo: :bar, 47 | }, 48 | }, 49 | } 50 | 51 | assert_equal({ 'a' => { 'b' => { 'foo' => 'bar' } } }, JSON::Schema.stringify(hash), 'symbols in a nested hash of hashes should be converted to strings') 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/ruby_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | -------------------------------------------------------------------------------- /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/support/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'webmock/minitest' 5 | 6 | $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__)) 7 | require 'json-schema' 8 | 9 | Dir[File.join(File.expand_path(__dir__), '*.rb')].each do |support_file| 10 | require support_file unless support_file == __FILE__ 11 | end 12 | 13 | module Minitest 14 | class Test 15 | def suppress_warnings 16 | old_verbose = $VERBOSE 17 | $VERBOSE = nil 18 | begin 19 | yield 20 | ensure 21 | $VERBOSE = old_verbose 22 | end 23 | end 24 | 25 | def schema_fixture_path(filename) 26 | File.join(File.dirname(__FILE__), '../schemas', filename) 27 | end 28 | 29 | def data_fixture_path(filename) 30 | File.join(File.dirname(__FILE__), '../data', filename) 31 | end 32 | 33 | def assert_valid(schema, data, options = {}, msg = "#{data.inspect} should be valid for schema:\n#{schema.inspect}") 34 | errors = validation_errors(schema, data, options) 35 | 36 | assert_empty(errors, msg) 37 | end 38 | 39 | def refute_valid(schema, data, options = {}, msg = "#{data.inspect} should be invalid for schema:\n#{schema.inspect}") 40 | errors = validation_errors(schema, data, options) 41 | 42 | refute_equal([], errors, msg) 43 | end 44 | 45 | def validation_errors(schema, data, options) 46 | options = { clear_cache: true, validate_schema: true }.merge(options) 47 | JSON::Validator.fully_validate(schema, data, options) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/bad_schema_ref_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | require 'socket' 3 | 4 | class BadSchemaRefTest < Minitest::Test 5 | def setup 6 | WebMock.allow_net_connect! 7 | end 8 | 9 | def teardown 10 | WebMock.disable_net_connect! 11 | end 12 | 13 | def test_bad_uri_ref 14 | schema = { 15 | '$schema' => 'http://json-schema.org/draft-04/schema#', 16 | 'type' => 'array', 17 | 'items' => { '$ref' => '../google.json' }, 18 | } 19 | 20 | data = [1, 2, 3] 21 | error = assert_raises(JSON::Schema::ReadFailed) do 22 | JSON::Validator.validate(schema, data) 23 | end 24 | 25 | expanded_path = File.expand_path('../google.json', __dir__) 26 | 27 | assert_equal(:file, error.type) 28 | assert_equal(expanded_path, error.location) 29 | assert_equal("Read of file at #{expanded_path} failed", error.message) 30 | end 31 | 32 | def test_bad_host_ref 33 | schema = { 34 | '$schema' => 'http://json-schema.org/draft-04/schema#', 35 | 'type' => 'array', 36 | 'items' => { '$ref' => 'http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema' }, 37 | } 38 | 39 | data = [1, 2, 3] 40 | error = assert_raises(JSON::Schema::ReadFailed) do 41 | JSON::Validator.validate(schema, data) 42 | end 43 | 44 | assert_equal(:uri, error.type) 45 | assert_equal('http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema', error.location) 46 | assert_equal('Read of URI at http://ppcheesecheseunicornnuuuurrrrr.example.invalid/json.schema failed', error.message) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/extends_nested_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 13 | assert_valid path, { 'outerC' => true }, {}, 'Outer defn is broken, maybe the outer extends overrode it' 14 | end 15 | end 16 | 17 | def test_valid_outer_extended 18 | ALL_SCHEMAS.each do |file| 19 | path = schema_fixture_path(file) 20 | 21 | assert_valid path, { 'innerA' => true }, {}, "Extends at the root level isn't working" 22 | end 23 | end 24 | 25 | def test_valid_inner 26 | ALL_SCHEMAS.each do |file| 27 | path = schema_fixture_path(file) 28 | 29 | assert_valid path, { 'outerB' => [{ 'innerA' => true }] }, {}, "Extends isn't working in the array element defn" 30 | end 31 | end 32 | 33 | def test_invalid_inner 34 | ALL_SCHEMAS.each do |file| 35 | path = schema_fixture_path(file) 36 | 37 | refute_valid path, { 'outerB' => [{ 'whaaaaat' => true }] }, {}, "Array element defn allowing anything when it should only allow what's in inner.schema" 38 | end 39 | end 40 | 41 | def test_invalid_outer 42 | path = schema_fixture_path(ADDITIONAL_PROPERTIES) 43 | 44 | refute_valid path, { 'whaaaaat' => true }, {}, "Outer defn allowing anything when it shouldn't" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/load_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | -------------------------------------------------------------------------------- /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 | 30 | message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{value}'" 31 | validation_error(processor, message, fragments, schema, attribute, options[:record_errors]) 32 | end 33 | 34 | def self.accept_value?(value) 35 | value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/any_of_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 17 | assert_equal(%i[anyof_0 anyof_1 anyof_2], nested_errors.keys, 'should have nested errors for each anyOf subschema') 18 | assert_match(%r{the property '#/names/0' value "jack" did not match the regex 'john'}i, nested_errors[:anyof_0][0][:message]) 19 | assert_match(%r{the property '#/names/0' value "jack" did not match the regex 'jane'}i, nested_errors[:anyof_1][0][:message]) 20 | assert_match(%r{the property '#/names/0' value "jack" did not match the regex 'jimmy'}i, nested_errors[:anyof_2][0][:message]) 21 | end 22 | 23 | def test_any_of_ref_message 24 | data = %({"names": ["jack"]}) 25 | errors = JSON::Validator.fully_validate(schema, data) 26 | expected_message = "The property '#/names/0' of type string did not match one or more of the required schemas. The schema specific errors were: 27 | 28 | - anyOf #0: 29 | - The property '#/names/0' value \"jack\" did not match the regex 'john' 30 | - anyOf #1: 31 | - The property '#/names/0' value \"jack\" did not match the regex 'jane' 32 | - anyOf #2: 33 | - The property '#/names/0' value \"jack\" did not match the regex 'jimmy'" 34 | 35 | assert_equal(expected_message, errors[0]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /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 = {}); end 7 | 8 | def self.build_fragment(fragments) 9 | "#/#{fragments.join('/')}" 10 | end 11 | 12 | def self.validation_error(processor, message, fragments, current_schema, failed_attribute, record_errors, properties = []) 13 | error = ValidationError.new(message, fragments, failed_attribute, current_schema, properties) 14 | if record_errors 15 | processor.validation_error(error) 16 | else 17 | raise error 18 | end 19 | end 20 | 21 | def self.validation_errors(validator) 22 | validator.validation_errors 23 | end 24 | 25 | TYPE_CLASS_MAPPINGS = { 26 | 'string' => String, 27 | 'number' => Numeric, 28 | 'integer' => Integer, 29 | 'boolean' => [TrueClass, FalseClass], 30 | 'object' => Hash, 31 | 'array' => Array, 32 | 'null' => NilClass, 33 | 'any' => Object, 34 | } 35 | 36 | def self.data_valid_for_type?(data, type) 37 | valid_classes = TYPE_CLASS_MAPPINGS.fetch(type) { return true } 38 | Array(valid_classes).any? { |c| data.is_a?(c) } 39 | end 40 | 41 | # Lookup Schema type of given class instance 42 | def self.type_of_data(data) 43 | type, = TYPE_CLASS_MAPPINGS.map { |k, v| [k, v] }.sort_by do |(_, v)| 44 | -Array(v).map { |klass| klass.ancestors.size }.max 45 | end.find do |(_, v)| 46 | Array(v).any? { |klass| data.is_a?(klass) } 47 | end 48 | type 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/validator_schema_reader_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class ValidatorSchemaReaderTest < Minitest::Test 4 | class MockReader < JSON::Schema::Reader 5 | def read(location) 6 | return super unless location.to_s == 'http://any.url/at/all' 7 | 8 | schema = { 9 | '$schema' => 'http://json-schema.org/draft-04/schema#', 10 | 'type' => 'string', 11 | 'minLength' => 2, 12 | } 13 | 14 | JSON::Schema.new(schema, Addressable::URI.parse(location.to_s)) 15 | end 16 | end 17 | 18 | def setup 19 | @original_reader = JSON::Validator.schema_reader 20 | end 21 | 22 | def teardown 23 | JSON::Validator.schema_reader = @original_reader 24 | end 25 | 26 | def test_default_schema_reader 27 | reader = JSON::Validator.schema_reader 28 | 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 | 38 | assert_valid schema, 'abc' 39 | refute_valid schema, 'a' 40 | end 41 | 42 | def test_validate_with_reader 43 | reader = MockReader.new 44 | schema = { '$ref' => 'http://any.url/at/all' } 45 | 46 | assert_valid schema, 'abc', schema_reader: reader 47 | refute_valid schema, 'a', schema_reader: reader 48 | end 49 | 50 | def test_validate_list_with_reader 51 | reader = MockReader.new 52 | schema = { '$ref' => 'http://any.url/at/all' } 53 | 54 | assert_valid schema, %w[abc def], schema_reader: reader, list: true 55 | refute_valid schema, %w[abc a], schema_reader: reader, list: true 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft1.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | class Draft1 < Validator 6 | def initialize 7 | super 8 | @attributes = { 9 | 'type' => JSON::Schema::TypeAttribute, 10 | 'disallow' => JSON::Schema::DisallowAttribute, 11 | 'format' => JSON::Schema::FormatAttribute, 12 | 'maximum' => JSON::Schema::MaximumInclusiveAttribute, 13 | 'minimum' => JSON::Schema::MinimumInclusiveAttribute, 14 | 'minItems' => JSON::Schema::MinItemsAttribute, 15 | 'maxItems' => JSON::Schema::MaxItemsAttribute, 16 | 'minLength' => JSON::Schema::MinLengthAttribute, 17 | 'maxLength' => JSON::Schema::MaxLengthAttribute, 18 | 'maxDecimal' => JSON::Schema::MaxDecimalAttribute, 19 | 'enum' => JSON::Schema::EnumAttribute, 20 | 'properties' => JSON::Schema::PropertiesOptionalAttribute, 21 | 'pattern' => JSON::Schema::PatternAttribute, 22 | 'additionalProperties' => JSON::Schema::AdditionalPropertiesAttribute, 23 | 'items' => JSON::Schema::ItemsAttribute, 24 | 'extends' => JSON::Schema::ExtendsAttribute, 25 | } 26 | @default_formats = { 27 | 'date-time' => DateTimeFormat, 28 | 'date' => DateFormat, 29 | 'time' => TimeFormat, 30 | 'ip-address' => IP4Format, 31 | 'ipv6' => IP6Format, 32 | 'uri' => UriFormat, 33 | } 34 | @formats = @default_formats.clone 35 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-01/schema#') 36 | @names = ['draft1'] 37 | @metaschema_name = 'draft-01.json' 38 | end 39 | 40 | JSON::Validator.register_validator(new) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft2.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | class Draft2 < Validator 6 | def initialize 7 | super 8 | @attributes = { 9 | 'type' => JSON::Schema::TypeAttribute, 10 | 'disallow' => JSON::Schema::DisallowAttribute, 11 | 'format' => JSON::Schema::FormatAttribute, 12 | 'maximum' => JSON::Schema::MaximumInclusiveAttribute, 13 | 'minimum' => JSON::Schema::MinimumInclusiveAttribute, 14 | 'minItems' => JSON::Schema::MinItemsAttribute, 15 | 'maxItems' => JSON::Schema::MaxItemsAttribute, 16 | 'uniqueItems' => JSON::Schema::UniqueItemsAttribute, 17 | 'minLength' => JSON::Schema::MinLengthAttribute, 18 | 'maxLength' => JSON::Schema::MaxLengthAttribute, 19 | 'divisibleBy' => JSON::Schema::DivisibleByAttribute, 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-02/schema#') 37 | @names = ['draft2'] 38 | @metaschema_name = 'draft-02.json' 39 | end 40 | 41 | JSON::Validator.register_validator(new) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /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 | 20 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 21 | return 22 | end 23 | 24 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m.length < 4 25 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[1].to_i > 23 26 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[2].to_i > 59 27 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) and return if m[3].to_i > 59 28 | else 29 | validation_error(processor, error_message, fragments, current_schema, self, options[:record_errors]) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /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, :properties 6 | 7 | def initialize(message, fragments, failed_attribute, schema, properties = []) 8 | @fragments = fragments.clone 9 | @schema = schema 10 | @sub_errors = {} 11 | @failed_attribute = failed_attribute 12 | @message = message 13 | @properties = properties 14 | super(message_with_schema) 15 | end 16 | 17 | def to_string(subschema_level = 0) 18 | if @sub_errors.empty? 19 | (subschema_level == 0) ? message_with_schema : message 20 | else 21 | messages = ["#{message}. The schema specific errors were:\n"] 22 | @sub_errors.each do |subschema, errors| 23 | messages.push "- #{subschema}:" 24 | messages.concat(Array(errors).map { |e| "#{INDENT}- #{e.to_string(subschema_level + 1)}" }) 25 | end 26 | messages.map { |m| (INDENT * subschema_level) + m }.join("\n") 27 | end 28 | end 29 | 30 | def to_hash 31 | 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 } 32 | unless @sub_errors.empty? 33 | base[:errors] = @sub_errors.each_with_object({}) do |(subschema, errors), hsh| 34 | subschema_sym = subschema.downcase.gsub(/\W+/, '_').to_sym 35 | hsh[subschema_sym] = Array(errors).map { |e| e.to_hash } 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 | -------------------------------------------------------------------------------- /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] unless 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}' 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 = nil 26 | schema = nil 27 | 28 | if s.is_a?(Hash) 29 | uri = current_schema.uri 30 | if s['$ref'] 31 | ref_uri, ref_schema = JSON::Schema::RefAttribute.get_referenced_uri_and_schema(s, current_schema, validator) 32 | if ref_schema 33 | if s.size == 1 # Check if anything else apart from $ref 34 | uri = ref_uri 35 | schema = ref_schema 36 | else 37 | s = s.dup 38 | s.delete '$ref' 39 | s = ref_schema.schema.merge(s) 40 | end 41 | end 42 | end 43 | schema ||= JSON::Schema.new(s, uri, validator) 44 | end 45 | 46 | [uri, schema] 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/merge_missing_values_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 9 | assert_equal('foo', original) 10 | end 11 | 12 | def test_merge_missing_values_for_empty_array 13 | original = [] 14 | updated = [] 15 | JSON::Validator.merge_missing_values(updated, original) 16 | 17 | assert_empty(original) 18 | end 19 | 20 | def test_merge_missing_values_for_empty_hash 21 | original = {} 22 | updated = {} 23 | JSON::Validator.merge_missing_values(updated, original) 24 | 25 | assert_empty(original) 26 | end 27 | 28 | def test_merge_missing_values_for_new_values 29 | original = { hello: 'world' } 30 | updated = { 'hello' => 'world', 'foo' => 'bar' } 31 | JSON::Validator.merge_missing_values(updated, original) 32 | 33 | assert_equal({ :hello => 'world', 'foo' => 'bar' }, original) 34 | end 35 | 36 | def test_merge_missing_values_for_nested_array 37 | original = [:hello, 'world', 1, 2, 3, { :foo => :bar, 'baz' => 'qux' }] 38 | updated = ['hello', 'world', 1, 2, 3, { 'foo' => 'bar', 'baz' => 'qux', 'this_is' => 'new' }] 39 | JSON::Validator.merge_missing_values(updated, original) 40 | 41 | assert_equal([:hello, 'world', 1, 2, 3, { :foo => :bar, 'baz' => 'qux', 'this_is' => 'new' }], original) 42 | end 43 | 44 | def test_merge_missing_values_for_nested_hash 45 | original = { hello: 'world', foo: ['bar', :baz, { uno: { due: 3 } }] } 46 | updated = { 'hello' => 'world', 'foo' => ['bar', 'baz', { 'uno' => { 'due' => 3, 'this_is' => 'new' } }], 'ack' => 'sed' } 47 | JSON::Validator.merge_missing_values(updated, original) 48 | 49 | assert_equal({ :hello => 'world', :foo => ['bar', :baz, { uno: { :due => 3, 'this_is' => 'new' } }], 'ack' => 'sed' }, original) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/uri_parsing_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class UriParsingTest < Minitest::Test 4 | def test_asian_characters 5 | schema = { 6 | '$schema' => 'http://json-schema.org/draft-04/schema#', 7 | 'id' => 'http://俺:鍵@例え.テスト/p?条件#ここ#', 8 | 'type' => 'object', 9 | 'required' => ['a'], 10 | 'properties' => { 11 | 'a' => { 12 | 'id' => 'a', 13 | 'type' => 'integer', 14 | }, 15 | }, 16 | } 17 | data = { 'a' => 5 } 18 | 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 | 40 | assert_valid schema, data 41 | end 42 | 43 | def test_schema_ref_from_file_with_spaces 44 | schema = { 45 | '$schema' => 'http://json-schema.org/draft-04/schema#', 46 | 'type' => 'object', 47 | 'required' => ['names'], 48 | 'properties' => { 49 | 'names' => { 50 | 'type' => 'array', 51 | 'items' => { 52 | 'anyOf' => [ 53 | { '$ref' => 'test/schemas/ref john with spaces schema.json' }, 54 | ], 55 | }, 56 | }, 57 | }, 58 | } 59 | data = { 'names' => [{ 'first' => 'john' }] } 60 | 61 | assert_valid schema, data 62 | end 63 | 64 | def test_schema_from_file_with_spaces 65 | data = { 'first' => 'john' } 66 | schema = 'test/schemas/ref john with spaces schema.json' 67 | 68 | assert_valid schema, data 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/fragment_validation_with_ref_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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' => %w[content author], 49 | }, 50 | }, 51 | ], 52 | }, 53 | }, 54 | } 55 | end 56 | 57 | def test_validation_of_fragment 58 | data = [{ 'content' => 'ohai', 'author' => 'Bob' }] 59 | 60 | assert_valid whole_schema, data, fragment: '#/definitions/posts' 61 | end 62 | 63 | def test_validation_of_fragment_with_array 64 | data = { 'content' => 'ohai', 'author' => 'Bob' } 65 | 66 | assert_valid(whole_schema_with_array, data, 67 | fragment: '#/definitions/omg/links/0/schema') 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/json-schema/schema.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module JSON 4 | class Schema 5 | attr_accessor :schema, :uri, :validator 6 | 7 | def initialize(schema, uri, parent_validator = nil) 8 | @schema = schema 9 | @uri = uri 10 | 11 | # If there is an ID on this schema, use it to generate the URI 12 | if @schema['id'].is_a?(String) 13 | temp_uri = JSON::Util::URI.parse(@schema['id']) 14 | if temp_uri.relative? 15 | temp_uri = uri.join(temp_uri) 16 | end 17 | @uri = temp_uri 18 | end 19 | @uri = JSON::Util::URI.strip_fragment(@uri) 20 | 21 | # If there is a $schema on this schema, use it to determine which validator to use 22 | @validator = if @schema['$schema'] 23 | JSON::Validator.validator_for_uri(@schema['$schema']) 24 | elsif parent_validator 25 | parent_validator 26 | else 27 | JSON::Validator.default_validator 28 | end 29 | end 30 | 31 | def validate(data, fragments, processor, options = {}) 32 | @validator.validate(self, data, fragments, processor, options) 33 | end 34 | 35 | def self.stringify(schema) 36 | case schema 37 | when Hash 38 | schema.map { |key, _value| [key.to_s, stringify(schema[key])] }.to_h 39 | when Array 40 | schema.map do |schema_item| 41 | stringify(schema_item) 42 | end 43 | when Symbol 44 | schema.to_s 45 | else 46 | schema 47 | end 48 | end 49 | 50 | # @return [JSON::Schema] a new schema matching an array whose items all match this schema. 51 | def to_array_schema 52 | array_schema = { 'type' => 'array', 'items' => schema } 53 | array_schema['$schema'] = schema['$schema'] unless schema['$schema'].nil? 54 | self.class.new(array_schema, uri, validator) 55 | end 56 | 57 | def to_s 58 | @schema.to_json 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /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 -= 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 | unless 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/all_of_ref_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 24 | assert_equal([:allof_0], nested_errors.keys, 'should have nested errors for each allOf subschema') 25 | assert_match(%r{the property '#/name' of type string did not match the following type: integer}i, nested_errors[:allof_0][0][:message]) 26 | end 27 | 28 | def test_all_of_ref_message 29 | errors = JSON::Validator.fully_validate(schema, data) 30 | expected_message = "The property '#/' of type object did not match all of the required schemas. The schema specific errors were: 31 | 32 | - allOf #0: 33 | - The property '#/name' of type string did not match the following type: integer" 34 | 35 | assert_equal(expected_message, errors[0]) 36 | end 37 | 38 | def test_all_of_ref_message_with_one_attribute_wrong 39 | errors = JSON::Validator.fully_validate(schema, data) 40 | expected_message = "The property '#/' of type object did not match all of the required schemas. The schema specific errors were: 41 | 42 | - allOf #0: 43 | - The property '#/name' of type string did not match the following type: integer" 44 | 45 | assert_equal(expected_message, errors[0]) 46 | end 47 | 48 | def test_all_of_ref_validate_messgae 49 | exception = assert_raises JSON::Schema::ValidationError do 50 | JSON::Validator.validate!(schema, data) 51 | end 52 | expected_error_message = "The property '#/name' of type string did not match the following type: integer" 53 | 54 | assert_equal expected_error_message, exception.message 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 | class Draft3 < Validator 6 | def initialize 7 | super 8 | @attributes = { 9 | 'type' => JSON::Schema::TypeAttribute, 10 | 'disallow' => JSON::Schema::DisallowAttribute, 11 | 'format' => JSON::Schema::FormatAttribute, 12 | 'maximum' => JSON::Schema::MaximumAttribute, 13 | 'minimum' => JSON::Schema::MinimumAttribute, 14 | 'minItems' => JSON::Schema::MinItemsAttribute, 15 | 'maxItems' => JSON::Schema::MaxItemsAttribute, 16 | 'uniqueItems' => JSON::Schema::UniqueItemsAttribute, 17 | 'minLength' => JSON::Schema::MinLengthAttribute, 18 | 'maxLength' => JSON::Schema::MaxLengthAttribute, 19 | 'divisibleBy' => JSON::Schema::DivisibleByAttribute, 20 | 'enum' => JSON::Schema::EnumAttribute, 21 | 'properties' => JSON::Schema::PropertiesAttribute, 22 | 'pattern' => JSON::Schema::PatternAttribute, 23 | 'patternProperties' => JSON::Schema::PatternPropertiesAttribute, 24 | 'additionalProperties' => JSON::Schema::AdditionalPropertiesAttribute, 25 | 'items' => JSON::Schema::ItemsAttribute, 26 | 'additionalItems' => JSON::Schema::AdditionalItemsAttribute, 27 | 'dependencies' => JSON::Schema::DependenciesAttribute, 28 | 'extends' => JSON::Schema::ExtendsAttribute, 29 | '$ref' => JSON::Schema::RefAttribute, 30 | } 31 | @default_formats = { 32 | 'date-time' => DateTimeFormat, 33 | 'date' => DateFormat, 34 | 'ip-address' => IP4Format, 35 | 'ipv6' => IP6Format, 36 | 'time' => TimeFormat, 37 | 'uri' => UriFormat, 38 | } 39 | @formats = @default_formats.clone 40 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-03/schema#') 41 | @names = ['draft3', 'http://json-schema.org/draft-03/schema#'] 42 | @metaschema_name = 'draft-03.json' 43 | end 44 | 45 | JSON::Validator.register_validator(new) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 unless valid 31 | while diff > 0 32 | diff -= 1 33 | errors["oneOf ##{schema_index}"].push(validation_errors(processor).pop) 34 | end 35 | data = original_data 36 | end 37 | 38 | if validation_error_count == one_of.length - 1 39 | data = success_data 40 | return 41 | end 42 | 43 | message = if validation_error_count == one_of.length 44 | "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match any of the required schemas" 45 | else 46 | "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} matched more than one of the required schemas" 47 | end 48 | 49 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) if message 50 | validation_errors(processor).last.sub_errors = errors if message 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | 4 | desc 'Updates the json-schema common test suite to the latest version' 5 | task :update_common_tests do 6 | unless File.read('.git/config').include?('submodule "test/test-suite"') 7 | sh 'git submodule init' 8 | end 9 | 10 | puts 'Updating json-schema common test suite...' 11 | 12 | begin 13 | sh 'git submodule update --remote --quiet' 14 | rescue StandardError 15 | warn 'Failed to update common test suite.' 16 | end 17 | end 18 | 19 | desc 'Update meta-schemas to the latest version' 20 | task :update_meta_schemas do 21 | puts 'Updating meta-schemas...' 22 | 23 | require 'open-uri' 24 | require 'thwait' 25 | 26 | download_threads = Dir['resources/*.json'].map do |path| 27 | schema_uri = File.read(path)[/"\$?id"\s*:\s*"(.*?)"/, 1] 28 | 29 | Thread.new(schema_uri) do |uri| 30 | Thread.current[:uri] = uri 31 | 32 | begin 33 | metaschema = URI(uri).read 34 | 35 | File.write(path, metaschema) 36 | rescue StandardError 37 | false 38 | end 39 | end 40 | end 41 | 42 | ThreadsWait.all_waits(*download_threads) do |t| 43 | if t.value 44 | puts t[:uri] 45 | else 46 | warn "Failed to update meta-schema #{t[:uri]}" 47 | end 48 | end 49 | end 50 | 51 | Rake::TestTask.new do |t| 52 | t.libs << '.' 53 | t.warning = true 54 | t.verbose = true 55 | t.test_files = FileList.new('test/*_test.rb') 56 | end 57 | 58 | task update: %i[update_common_tests update_meta_schemas] 59 | 60 | begin 61 | require 'voxpupuli/rubocop/rake' 62 | rescue LoadError 63 | # the voxpupuli-rubocop gem is optional 64 | end 65 | 66 | task default: :test 67 | 68 | begin 69 | require 'rubygems' 70 | require 'github_changelog_generator/task' 71 | rescue LoadError 72 | else 73 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 74 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog dependencies] 75 | config.user = 'voxpupuli' 76 | config.project = 'json-schema' 77 | gem_version = Gem::Specification.load("#{config.project}.gemspec").version 78 | config.future_release = "v#{gem_version}" 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /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/common_test_suite_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | require 'json' 3 | 4 | class CommonTestSuiteTest < Minitest::Test 5 | TEST_DIR = File.expand_path('test-suite/tests', __dir__) 6 | 7 | IGNORED_TESTS = YAML.load_file(File.expand_path('support/test_suite_ignored_tests.yml', __dir__)) 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..] 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 | -------------------------------------------------------------------------------- /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 | 23 | assert_valid schema, { 'a' => value } 24 | 25 | other_values.each do |other_value| 26 | refute_valid schema, { 'a' => other_value } 27 | end 28 | end 29 | 30 | define_method(:"test_#{name}_type_value") do 31 | schema = { 'type' => name } 32 | 33 | assert_valid schema, value 34 | 35 | other_values.each do |other_value| 36 | schema = { 'type' => name } 37 | 38 | refute_valid schema, other_value 39 | end 40 | end 41 | end 42 | 43 | def test_type_union 44 | schema = { 'type' => %w[integer string] } 45 | 46 | assert_valid schema, 5 47 | assert_valid schema, 'str' 48 | refute_valid schema, nil 49 | refute_valid schema, [5, 'str'] 50 | end 51 | end 52 | 53 | # The draft1..3 schemas support an additional type, `any`. 54 | module AnyTypeTests 55 | def test_any_type 56 | schema = { 'type' => 'any' } 57 | 58 | SimpleTypeTests::TYPES.each_value do |value| 59 | assert_valid schema, value 60 | end 61 | end 62 | end 63 | 64 | # The draft1..3 schemas support schemas as values for `type`. 65 | module SchemaUnionTypeTests 66 | def test_union_type_with_schemas 67 | schema = { 68 | 'properties' => { 69 | 'a' => { 70 | 'type' => [ 71 | { 'type' => 'string' }, 72 | { 'type' => 'object', 'properties' => { 'b' => { 'type' => 'integer' } } }, 73 | ], 74 | }, 75 | }, 76 | } 77 | 78 | assert_valid schema, { 'a' => 'test' } 79 | refute_valid schema, { 'a' => 5 } 80 | 81 | assert_valid schema, { 'a' => { 'b' => 5 } } 82 | refute_valid schema, { 'a' => { 'b' => 'taco' } } 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/files_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class FilesTest < Minitest::Test 4 | # 5 | # These tests are ONLY run if there is an appropriate JSON backend parser available 6 | # 7 | 8 | def test_schema_from_file 9 | assert_valid schema_fixture_path('good_schema_1.json'), { 'a' => 5 } 10 | refute_valid schema_fixture_path('good_schema_1.json'), { 'a' => 'bad' } 11 | end 12 | 13 | def test_data_from_file_v3 14 | schema = { '$schema' => 'http://json-schema.org/draft-03/schema#', 'type' => 'object', 'properties' => { 'a' => { 'type' => 'integer' } } } 15 | 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 | 23 | assert_valid schema, '{"a": 5}', json: true 24 | refute_valid schema, '{"a": "poop"}', json: true 25 | end 26 | 27 | def test_data_from_file_v4 28 | schema = { '$schema' => 'http://json-schema.org/draft-04/schema#', 'type' => 'object', 'properties' => { 'a' => { 'type' => 'integer' } } } 29 | 30 | assert_valid schema, data_fixture_path('good_data_1.json'), uri: true 31 | refute_valid schema, data_fixture_path('bad_data_1.json'), uri: true 32 | end 33 | 34 | def test_data_from_json_v4 35 | schema = { '$schema' => 'http://json-schema.org/draft-04/schema#', 'type' => 'object', 'properties' => { 'a' => { 'type' => 'integer' } } } 36 | 37 | assert_valid schema, '{"a": 5}', json: true 38 | refute_valid schema, '{"a": "poop"}', json: true 39 | end 40 | 41 | def test_both_from_file 42 | assert_valid schema_fixture_path('good_schema_1.json'), data_fixture_path('good_data_1.json'), uri: true 43 | refute_valid schema_fixture_path('good_schema_1.json'), data_fixture_path('bad_data_1.json'), uri: true 44 | end 45 | 46 | def test_file_ref 47 | assert_valid schema_fixture_path('good_schema_2.json'), { 'b' => { 'a' => 5 } } 48 | refute_valid schema_fixture_path('good_schema_1.json'), { 'b' => { 'a' => 'boo' } } 49 | end 50 | 51 | def test_file_extends 52 | assert_valid schema_fixture_path('good_schema_extends1.json'), { 'a' => 5 } 53 | assert_valid schema_fixture_path('good_schema_extends2.json'), { 'a' => 5, 'b' => { 'a' => 5 } } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /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 -= schema['properties'].keys 34 | end 35 | 36 | schema['patternProperties']&.each_key do |key| 37 | regexp = Regexp.new(key) 38 | extra_properties.reject! { |prop| regexp.match(prop) } 39 | end 40 | 41 | if extended_schemas = schema['extends'] 42 | extended_schemas = [extended_schemas] unless extended_schemas.is_a?(Array) 43 | extended_schemas.each do |schema_value| 44 | _, extended_schema = JSON::Schema::ExtendsAttribute.get_extended_uri_and_schema(schema_value, current_schema, validator) 45 | if extended_schema 46 | extra_properties = remove_valid_properties(extra_properties, extended_schema, validator) 47 | end 48 | end 49 | end 50 | 51 | extra_properties 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/json-schema/validators/draft4.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | class Draft4 < Validator 6 | def initialize 7 | super 8 | @attributes = { 9 | 'type' => JSON::Schema::TypeV4Attribute, 10 | 'allOf' => JSON::Schema::AllOfAttribute, 11 | 'anyOf' => JSON::Schema::AnyOfAttribute, 12 | 'oneOf' => JSON::Schema::OneOfAttribute, 13 | 'not' => JSON::Schema::NotAttribute, 14 | 'disallow' => JSON::Schema::DisallowAttribute, 15 | 'format' => JSON::Schema::FormatAttribute, 16 | 'maximum' => JSON::Schema::MaximumAttribute, 17 | 'minimum' => JSON::Schema::MinimumAttribute, 18 | 'minItems' => JSON::Schema::MinItemsAttribute, 19 | 'maxItems' => JSON::Schema::MaxItemsAttribute, 20 | 'minProperties' => JSON::Schema::MinPropertiesAttribute, 21 | 'maxProperties' => JSON::Schema::MaxPropertiesAttribute, 22 | 'uniqueItems' => JSON::Schema::UniqueItemsAttribute, 23 | 'minLength' => JSON::Schema::MinLengthAttribute, 24 | 'maxLength' => JSON::Schema::MaxLengthAttribute, 25 | 'multipleOf' => JSON::Schema::MultipleOfAttribute, 26 | 'enum' => JSON::Schema::EnumAttribute, 27 | 'properties' => JSON::Schema::PropertiesV4Attribute, 28 | 'required' => JSON::Schema::RequiredAttribute, 29 | 'pattern' => JSON::Schema::PatternAttribute, 30 | 'patternProperties' => JSON::Schema::PatternPropertiesAttribute, 31 | 'additionalProperties' => JSON::Schema::AdditionalPropertiesAttribute, 32 | 'items' => JSON::Schema::ItemsAttribute, 33 | 'additionalItems' => JSON::Schema::AdditionalItemsAttribute, 34 | 'dependencies' => JSON::Schema::DependenciesV4Attribute, 35 | 'extends' => JSON::Schema::ExtendsAttribute, 36 | '$ref' => JSON::Schema::RefAttribute, 37 | } 38 | @default_formats = { 39 | 'date-time' => DateTimeV4Format, 40 | 'ipv4' => IP4Format, 41 | 'ipv6' => IP6Format, 42 | 'uri' => UriFormat, 43 | } 44 | @formats = @default_formats.clone 45 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-04/schema#') 46 | @names = ['draft4', 'http://json-schema.org/draft-04/schema#'] 47 | @metaschema_name = 'draft-04.json' 48 | end 49 | 50 | JSON::Validator.register_validator(new) 51 | JSON::Validator.register_default_validator(new) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /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}' 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 = nil 24 | schema = nil 25 | 26 | temp_uri = JSON::Util::URI.normalize_ref(s['$ref'], current_schema.uri) 27 | 28 | # Grab the parent schema from the schema list 29 | schema_key = temp_uri.to_s.split('#')[0] + '#' 30 | 31 | ref_schema = JSON::Validator.schema_for_uri(schema_key) 32 | 33 | if ref_schema 34 | # Perform fragment resolution to retrieve the appropriate level for the schema 35 | target_schema = ref_schema.schema 36 | fragments = JSON::Util::URI.parse(JSON::Util::URI.unescape_uri(temp_uri)).fragment.split('/') 37 | fragment_path = '' 38 | fragments.each do |fragment| 39 | next unless fragment && fragment != '' 40 | 41 | fragment = fragment.gsub('~0', '~').gsub('~1', '/') 42 | target_schema = if target_schema.is_a?(Array) 43 | target_schema[fragment.to_i] 44 | else 45 | target_schema[fragment] 46 | end 47 | fragment_path += "/#{fragment}" 48 | if target_schema.nil? 49 | raise SchemaError, "The fragment '#{fragment_path}' does not exist on schema #{ref_schema.uri}" 50 | end 51 | end 52 | 53 | # We have the schema finally, build it and validate! 54 | uri = temp_uri 55 | schema = JSON::Schema.new(target_schema, temp_uri, validator) 56 | end 57 | 58 | [uri, schema] 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/extended_schema_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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/validators/draft6.rb: -------------------------------------------------------------------------------- 1 | require 'json-schema/schema/validator' 2 | 3 | module JSON 4 | class Schema 5 | class Draft6 < Validator 6 | def initialize 7 | super 8 | @attributes = { 9 | 'type' => JSON::Schema::TypeV4Attribute, 10 | 'allOf' => JSON::Schema::AllOfAttribute, 11 | 'anyOf' => JSON::Schema::AnyOfAttribute, 12 | 'oneOf' => JSON::Schema::OneOfAttribute, 13 | 'not' => JSON::Schema::NotAttribute, 14 | 'disallow' => JSON::Schema::DisallowAttribute, 15 | 'format' => JSON::Schema::FormatAttribute, 16 | 'maximum' => JSON::Schema::MaximumAttribute, 17 | 'minimum' => JSON::Schema::MinimumAttribute, 18 | 'minItems' => JSON::Schema::MinItemsAttribute, 19 | 'maxItems' => JSON::Schema::MaxItemsAttribute, 20 | 'minProperties' => JSON::Schema::MinPropertiesAttribute, 21 | 'maxProperties' => JSON::Schema::MaxPropertiesAttribute, 22 | 'uniqueItems' => JSON::Schema::UniqueItemsAttribute, 23 | 'minLength' => JSON::Schema::MinLengthAttribute, 24 | 'maxLength' => JSON::Schema::MaxLengthAttribute, 25 | 'multipleOf' => JSON::Schema::MultipleOfAttribute, 26 | 'enum' => JSON::Schema::EnumAttribute, 27 | 'properties' => JSON::Schema::PropertiesV4Attribute, 28 | 'required' => JSON::Schema::RequiredAttribute, 29 | 'pattern' => JSON::Schema::PatternAttribute, 30 | 'patternProperties' => JSON::Schema::PatternPropertiesAttribute, 31 | 'additionalProperties' => JSON::Schema::AdditionalPropertiesAttribute, 32 | 'items' => JSON::Schema::ItemsAttribute, 33 | 'additionalItems' => JSON::Schema::AdditionalItemsAttribute, 34 | 'dependencies' => JSON::Schema::DependenciesV4Attribute, 35 | 'extends' => JSON::Schema::ExtendsAttribute, 36 | 'const' => JSON::Schema::ConstAttribute, 37 | '$ref' => JSON::Schema::RefAttribute, 38 | 'propertyNames' => JSON::Schema::PropertyNames, 39 | } 40 | @default_formats = { 41 | 'date-time' => DateTimeV4Format, 42 | 'ipv4' => IP4Format, 43 | 'ipv6' => IP6Format, 44 | 'uri' => UriFormat, 45 | } 46 | @formats = @default_formats.clone 47 | @uri = JSON::Util::URI.parse('http://json-schema.org/draft-06/schema#') 48 | @names = ['draft6', 'http://json-schema.org/draft-06/schema#'] 49 | @metaschema_name = 'draft-06.json' 50 | end 51 | 52 | JSON::Validator.register_validator(new) 53 | JSON::Validator.register_default_validator(new) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /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[:allPropertiesRequired] } 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 noAdditionalProperties is true, ensure no undefined properties exist in the data 37 | return unless options[:noAdditionalProperties] == 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 | unless diff.empty? 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], diff.keys) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/schema_reader_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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', __dir__) 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 | 31 | refute reader.accept_uri?(Addressable::URI.parse('http://foo.com')) 32 | end 33 | 34 | def test_refuse_all_files 35 | reader = JSON::Schema::Reader.new(accept_file: false) 36 | 37 | refute reader.accept_file?(Pathname.new('/foo/bar/baz')) 38 | end 39 | 40 | def test_accept_uri_proc 41 | reader = JSON::Schema::Reader.new( 42 | accept_uri: proc { |uri| uri.host == 'json-schema.org' }, 43 | ) 44 | 45 | assert reader.accept_uri?(Addressable::URI.parse('http://json-schema.org/address')) 46 | refute reader.accept_uri?(Addressable::URI.parse('http://sub.json-schema.org/address')) 47 | end 48 | 49 | def test_accept_file_proc 50 | test_root = Pathname.new(__FILE__).expand_path.dirname 51 | 52 | reader = JSON::Schema::Reader.new( 53 | accept_file: proc { |path| path.to_s.start_with?(test_root.to_s) }, 54 | ) 55 | 56 | assert reader.accept_file?(test_root.join('anything.json')) 57 | refute reader.accept_file?(test_root.join('..', 'anything.json')) 58 | end 59 | 60 | def test_file_scheme 61 | reader = JSON::Schema::Reader.new(accept_uri: true, accept_file: false) 62 | error = assert_raises(JSON::Schema::ReadRefused) do 63 | reader.read('file://' + ADDRESS_SCHEMA_PATH) 64 | end 65 | 66 | assert_equal(:file, error.type) 67 | assert_equal(ADDRESS_SCHEMA_PATH, error.location) 68 | assert_equal("Read of file at #{ADDRESS_SCHEMA_PATH} refused", error.message) 69 | end 70 | 71 | def test_parse_error 72 | stub_address_request('this is totally not valid JSON!') 73 | 74 | reader = JSON::Schema::Reader.new 75 | 76 | assert_raises(JSON::Schema::JsonParseError) do 77 | reader.read(ADDRESS_SCHEMA_URI) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/fragment_resolution_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 20 | refute_valid schema, data 21 | assert_valid schema, data, fragment: '#/properties/a' 22 | 23 | assert_raises JSON::Schema::SchemaError do 24 | JSON::Validator.validate!(schema, data, fragment: '/properties/a') 25 | end 26 | 27 | assert_raises JSON::Schema::SchemaError do 28 | JSON::Validator.validate!(schema, data, fragment: '#/properties/b') 29 | end 30 | end 31 | 32 | def test_odd_level_fragment_resolution 33 | schema = { 34 | 'foo' => { 35 | 'type' => 'object', 36 | 'required' => ['a'], 37 | 'properties' => { 38 | 'a' => { 'type' => 'integer' }, 39 | }, 40 | }, 41 | } 42 | 43 | assert_valid schema, { 'a' => 1 }, fragment: '#/foo' 44 | refute_valid schema, {}, fragment: '#/foo' 45 | end 46 | 47 | def test_even_level_fragment_resolution 48 | schema = { 49 | 'foo' => { 50 | 'bar' => { 51 | 'type' => 'object', 52 | 'required' => ['a'], 53 | 'properties' => { 54 | 'a' => { 'type' => 'integer' }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | assert_valid schema, { 'a' => 1 }, fragment: '#/foo/bar' 61 | refute_valid schema, {}, fragment: '#/foo/bar' 62 | end 63 | 64 | def test_array_fragment_resolution 65 | schema = { 66 | 'type' => 'object', 67 | 'required' => ['a'], 68 | 'properties' => { 69 | 'a' => { 70 | 'anyOf' => [ 71 | { 'type' => 'integer' }, 72 | { 'type' => 'string' }, 73 | ], 74 | }, 75 | }, 76 | } 77 | 78 | refute_valid schema, 'foo', fragment: '#/properties/a/anyOf/0' 79 | assert_valid schema, 'foo', fragment: '#/properties/a/anyOf/1' 80 | 81 | assert_valid schema, 5, fragment: '#/properties/a/anyOf/0' 82 | refute_valid schema, 5, fragment: '#/properties/a/anyOf/1' 83 | end 84 | 85 | def test_fragment_with_escape_sequences_resolution 86 | schema = { 87 | 'content' => { 88 | 'application/json' => { 89 | 'type' => 'object', 90 | 'required' => ['a'], 91 | 'properties' => { 92 | 'a' => { 'type' => 'integer' }, 93 | }, 94 | }, 95 | }, 96 | } 97 | 98 | assert_valid schema, { 'a' => 1 }, fragment: '#/content/application~1json' 99 | refute_valid schema, {}, fragment: '#/content/application~1json' 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/draft2_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 53 | assert_valid schema, data 54 | 55 | schema = { 56 | 'properties' => { 57 | 'a' => { 'type' => 'integer', 'optional' => 'true' }, 58 | }, 59 | } 60 | 61 | data = {} 62 | 63 | assert_valid schema, data 64 | end 65 | 66 | def test_disallow 67 | # Set up the default datatype 68 | schema = { 69 | 'properties' => { 70 | 'a' => { 'disallow' => 'integer' }, 71 | }, 72 | } 73 | 74 | data = { 75 | 'a' => nil, 76 | } 77 | 78 | data['a'] = 'string' 79 | 80 | assert_valid schema, data 81 | 82 | data['a'] = 5 83 | 84 | refute_valid schema, data 85 | 86 | schema['properties']['a']['disallow'] = %w[integer string] 87 | data['a'] = 'string' 88 | 89 | refute_valid schema, data 90 | 91 | data['a'] = 5 92 | 93 | refute_valid schema, data 94 | 95 | data['a'] = false 96 | 97 | assert_valid schema, data 98 | end 99 | 100 | def test_format_datetime 101 | schema = { 102 | 'type' => 'object', 103 | 'properties' => { 'a' => { 'type' => 'string', 'format' => 'date-time' } }, 104 | } 105 | 106 | assert_valid schema, { 'a' => '2010-01-01T12:00:00Z' } 107 | refute_valid schema, { 'a' => '2010-01-32T12:00:00Z' } 108 | refute_valid schema, { 'a' => '2010-13-01T12:00:00Z' } 109 | refute_valid schema, { 'a' => '2010-01-01T24:00:00Z' } 110 | refute_valid schema, { 'a' => '2010-01-01T12:60:00Z' } 111 | refute_valid schema, { 'a' => '2010-01-01T12:00:60Z' } 112 | refute_valid schema, { 'a' => '2010-01-01T12:00:00z' } 113 | refute_valid schema, { 'a' => '2010-01-0112:00:00Z' } 114 | refute_valid schema, { 'a' => "2010-01-01T12:00:00Z\nabc" } 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/one_of_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | 8 | assert_valid schema, data 9 | end 10 | 11 | def test_one_of_with_string_patterns 12 | schema = { 13 | '$schema' => 'http://json-schema.org/draft-04/schema#', 14 | 'oneOf' => [ 15 | { 16 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'foo' } }, 17 | }, 18 | { 19 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'bar' } }, 20 | }, 21 | { 22 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'baz' } }, 23 | }, 24 | ], 25 | } 26 | 27 | assert_valid schema, { 'a' => 'foo' } 28 | refute_valid schema, { 'a' => 'foobar' } 29 | assert_valid schema, { 'a' => 'baz' } 30 | refute_valid schema, { 'a' => 5 } 31 | end 32 | 33 | def test_one_of_sub_errors 34 | schema = { 35 | '$schema' => 'http://json-schema.org/draft-04/schema#', 36 | 'oneOf' => [ 37 | { 38 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'foo' } }, 39 | }, 40 | { 41 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'bar' } }, 42 | }, 43 | { 44 | 'properties' => { 'a' => { 'type' => 'number', 'minimum' => 10 } }, 45 | }, 46 | ], 47 | } 48 | 49 | errors = JSON::Validator.fully_validate(schema, { 'a' => 5 }, errors_as_objects: true) 50 | nested_errors = errors[0][:errors] 51 | 52 | assert_equal(%i[oneof_0 oneof_1 oneof_2], nested_errors.keys, 'should have nested errors for each allOf subschema') 53 | assert_match(%r{the property '#/a' of type Integer did not match the following type: string}i, nested_errors[:oneof_0][0][:message]) 54 | assert_match(%r{the property '#/a' did not have a minimum value of 10, inclusively}i, nested_errors[:oneof_2][0][:message]) 55 | end 56 | 57 | def test_one_of_sub_errors_message 58 | schema = { 59 | '$schema' => 'http://json-schema.org/draft-04/schema#', 60 | 'oneOf' => [ 61 | { 62 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'foo' } }, 63 | }, 64 | { 65 | 'properties' => { 'a' => { 'type' => 'string', 'pattern' => 'bar' } }, 66 | }, 67 | { 68 | 'properties' => { 'a' => { 'type' => 'number', 'minimum' => 10 } }, 69 | }, 70 | ], 71 | } 72 | 73 | errors = JSON::Validator.fully_validate(schema, { 'a' => 5 }) 74 | expected_message = "The property '#/' of type object did not match any of the required schemas. The schema specific errors were: 75 | 76 | - oneOf #0: 77 | - The property '#/a' of type integer did not match the following type: string 78 | - oneOf #1: 79 | - The property '#/a' of type integer did not match the following type: string 80 | - oneOf #2: 81 | - The property '#/a' did not have a minimum value of 10, inclusively" 82 | 83 | assert_equal(expected_message, errors[0]) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /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 | message = nil 11 | 12 | current_schema.schema['allOf'].each_with_index do |element, schema_index| 13 | schema = JSON::Schema.new(element, current_schema.uri, validator) 14 | 15 | # We're going to add a little cruft here to try and maintain any validation errors that occur in the allOf 16 | # We'll handle this by keeping an error count before and after validation, extracting those errors and pushing them onto an error array 17 | pre_validation_error_count = validation_errors(processor).count 18 | 19 | begin 20 | # Cannot raise if noAdditionalProperties is true, we need to 21 | # evaluate each sub schema within the allOf, before raising. 22 | if options[:noAdditionalProperties] == true 23 | schema.validate(data, fragments, processor, options.merge(record_errors: true)) 24 | else 25 | schema.validate(data, fragments, processor, options) 26 | end 27 | rescue ValidationError => e 28 | valid = false 29 | message = e.message 30 | end 31 | 32 | diff = validation_errors(processor).count - pre_validation_error_count 33 | 34 | while diff > 0 35 | diff -= 1 36 | errors["allOf ##{schema_index}"].push(validation_errors(processor).pop) 37 | end 38 | end 39 | 40 | # Find any properties that are missing across all subschemas. 41 | common_missing_properties = {} 42 | if options[:noAdditionalProperties] == true && !errors.empty? 43 | all_property_errors = errors.values.flatten.map(&:properties) 44 | common_missing_properties = (all_property_errors.first || []).to_set 45 | 46 | all_property_errors[1..].each do |curr_property_errors| 47 | common_missing_properties &= curr_property_errors.to_set 48 | end 49 | end 50 | 51 | # PropertiesV4Attribute represents errors that would indicate an 52 | # additional property was detected. If we filter these out, we should 53 | # be left with errors that are not dependent on any other sub schema. 54 | non_missing_property_errors = errors.values.flatten.reject do |error| 55 | error.failed_attribute == JSON::Schema::PropertiesV4Attribute 56 | end 57 | 58 | if !valid || !non_missing_property_errors.empty? || !common_missing_properties.empty? 59 | message ||= "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match all of the required schemas" 60 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 61 | validation_errors(processor).last.sub_errors = errors 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /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, %w[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, %w[a ab] 102 | refute_valid schema, %w[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 | types = if options[:disallow] 9 | current_schema.schema['disallow'] 10 | else 11 | current_schema.schema['type'] 12 | end 13 | 14 | unless 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 -= 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 unless valid 54 | 55 | message = "The property '#{build_fragment(fragments)}' matched one or more of the following types: #{list_types(types)}" 56 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 57 | elsif !valid 58 | if union 59 | 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)}" 60 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 61 | validation_errors(processor).last.sub_errors = union_errors 62 | else 63 | message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match the following type: #{list_types(types)}" 64 | validation_error(processor, message, fragments, current_schema, self, options[:record_errors]) 65 | end 66 | end 67 | end 68 | 69 | def self.list_types(types) 70 | types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ') 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | 12 | assert_valid schema, data 13 | end 14 | end 15 | 16 | module V3_V4 17 | def test_enum_optional 18 | schema = { 19 | 'properties' => { 20 | 'a' => { 'enum' => [1, 'boo', [1, 2, 3], { 'a' => 'b' }] }, 21 | }, 22 | } 23 | 24 | data = {} 25 | 26 | assert_valid schema, data 27 | end 28 | end 29 | 30 | module General 31 | def test_enum_general 32 | schema = { 33 | 'properties' => { 34 | 'a' => { 'enum' => [1, 'boo', [1, 2, 3], { 'a' => 'b' }] }, 35 | }, 36 | } 37 | 38 | data = { 'a' => 1 } 39 | 40 | assert_valid schema, data 41 | 42 | data['a'] = 'boo' 43 | 44 | assert_valid schema, data 45 | 46 | data['a'] = [1, 2, 3] 47 | 48 | assert_valid schema, data 49 | 50 | data['a'] = { 'a' => 'b' } 51 | 52 | assert_valid schema, data 53 | 54 | data['a'] = 'taco' 55 | 56 | refute_valid schema, data 57 | end 58 | 59 | def test_enum_number_integer_includes_float 60 | schema = { 61 | 'properties' => { 62 | 'a' => { 63 | 'type' => 'number', 64 | 'enum' => [0, 1, 2], 65 | }, 66 | }, 67 | } 68 | 69 | data = { 'a' => 0 } 70 | 71 | assert_valid schema, data 72 | 73 | data['a'] = 0.0 74 | 75 | assert_valid schema, data 76 | 77 | data['a'] = 1 78 | 79 | assert_valid schema, data 80 | 81 | data['a'] = 1.0 82 | 83 | assert_valid schema, data 84 | end 85 | 86 | def test_enum_number_float_includes_integer 87 | schema = { 88 | 'properties' => { 89 | 'a' => { 90 | 'type' => 'number', 91 | 'enum' => [0.0, 1.0, 2.0], 92 | }, 93 | }, 94 | } 95 | 96 | data = { 'a' => 0.0 } 97 | 98 | assert_valid schema, data 99 | 100 | data['a'] = 0 101 | 102 | assert_valid schema, data 103 | 104 | data['a'] = 1.0 105 | 106 | assert_valid schema, data 107 | 108 | data['a'] = 1 109 | 110 | assert_valid schema, data 111 | end 112 | 113 | def test_enum_integer_excludes_float 114 | schema = { 115 | 'properties' => { 116 | 'a' => { 117 | 'type' => 'integer', 118 | 'enum' => [0, 1, 2], 119 | }, 120 | }, 121 | } 122 | 123 | data = { 'a' => 0 } 124 | 125 | assert_valid schema, data 126 | 127 | data['a'] = 0.0 128 | 129 | refute_valid schema, data 130 | 131 | data['a'] = 1 132 | 133 | assert_valid schema, data 134 | 135 | data['a'] = 1.0 136 | 137 | refute_valid schema, data 138 | end 139 | 140 | def test_enum_with_schema_validation 141 | schema = { 142 | 'properties' => { 143 | 'a' => { 'enum' => [1, 'boo', [1, 2, 3], { 'a' => 'b' }] }, 144 | }, 145 | } 146 | data = { 'a' => 1 } 147 | 148 | assert_valid(schema, data, validate_schema: true) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Gem Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build-release: 13 | # Prevent releases from forked repositories 14 | if: github.repository_owner == 'voxpupuli' 15 | name: Build the gem 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Install Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 'ruby' 23 | - name: Build gem 24 | shell: bash 25 | run: gem build --verbose *.gemspec 26 | - name: Upload gem to GitHub cache 27 | uses: actions/upload-artifact@v6 28 | with: 29 | name: gem-artifact 30 | path: '*.gem' 31 | retention-days: 1 32 | compression-level: 0 33 | 34 | create-github-release: 35 | needs: build-release 36 | name: Create GitHub release 37 | runs-on: ubuntu-24.04 38 | permissions: 39 | contents: write # clone repo and create release 40 | steps: 41 | - name: Download gem from GitHub cache 42 | uses: actions/download-artifact@v7 43 | with: 44 | name: gem-artifact 45 | - name: Create Release 46 | shell: bash 47 | env: 48 | GH_TOKEN: ${{ github.token }} 49 | run: gh release create --repo ${{ github.repository }} ${{ github.ref_name }} --generate-notes *.gem 50 | 51 | release-to-github: 52 | needs: build-release 53 | name: Release to GitHub 54 | runs-on: ubuntu-24.04 55 | permissions: 56 | packages: write # publish to rubygems.pkg.github.com 57 | steps: 58 | - name: Download gem from GitHub cache 59 | uses: actions/download-artifact@v7 60 | with: 61 | name: gem-artifact 62 | - name: Publish gem to GitHub packages 63 | run: gem push --host https://rubygems.pkg.github.com/${{ github.repository_owner }} *.gem 64 | env: 65 | GEM_HOST_API_KEY: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | release-to-rubygems: 68 | needs: build-release 69 | name: Release gem to rubygems.org 70 | runs-on: ubuntu-24.04 71 | environment: release # recommended by rubygems.org 72 | permissions: 73 | id-token: write # rubygems.org authentication 74 | steps: 75 | - name: Download gem from GitHub cache 76 | uses: actions/download-artifact@v7 77 | with: 78 | name: gem-artifact 79 | - uses: rubygems/configure-rubygems-credentials@v1.0.0 80 | - name: Publish gem to rubygems.org 81 | shell: bash 82 | run: gem push *.gem 83 | 84 | release-verification: 85 | name: Check that all releases are done 86 | runs-on: ubuntu-24.04 87 | permissions: 88 | contents: read # minimal permissions that we have to grant 89 | needs: 90 | - create-github-release 91 | - release-to-github 92 | - release-to-rubygems 93 | steps: 94 | - name: Download gem from GitHub cache 95 | uses: actions/download-artifact@v7 96 | with: 97 | name: gem-artifact 98 | - name: Install Ruby 99 | uses: ruby/setup-ruby@v1 100 | with: 101 | ruby-version: 'ruby' 102 | - name: Wait for release to propagate 103 | shell: bash 104 | run: | 105 | gem install rubygems-await 106 | gem await *.gem 107 | -------------------------------------------------------------------------------- /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', __dir__) 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 | 47 | assert_valid schema, data 48 | 49 | schema = { 50 | 'properties' => { 51 | 'a' => { 'type' => 'integer', 'optional' => 'true' }, 52 | }, 53 | } 54 | 55 | data = {} 56 | 57 | assert_valid schema, data 58 | end 59 | 60 | def test_max_decimal 61 | # Set up the default datatype 62 | schema = { 63 | 'properties' => { 64 | 'a' => { 'maxDecimal' => 2 }, 65 | }, 66 | } 67 | 68 | data = { 69 | 'a' => nil, 70 | } 71 | 72 | data['a'] = 3.35 73 | 74 | assert_valid schema, data 75 | 76 | data['a'] = 3.455 77 | 78 | refute_valid schema, data 79 | 80 | schema['properties']['a']['maxDecimal'] = 0 81 | 82 | data['a'] = 4.0 83 | 84 | refute_valid schema, data 85 | 86 | data['a'] = 'boo' 87 | 88 | assert_valid schema, data 89 | 90 | data['a'] = 5 91 | 92 | assert_valid schema, data 93 | end 94 | 95 | def test_disallow 96 | # Set up the default datatype 97 | schema = { 98 | 'properties' => { 99 | 'a' => { 'disallow' => 'integer' }, 100 | }, 101 | } 102 | 103 | data = { 104 | 'a' => nil, 105 | } 106 | 107 | data['a'] = 'string' 108 | 109 | assert_valid schema, data 110 | 111 | data['a'] = 5 112 | 113 | refute_valid schema, data 114 | 115 | schema['properties']['a']['disallow'] = %w[integer string] 116 | data['a'] = 'string' 117 | 118 | refute_valid schema, data 119 | 120 | data['a'] = 5 121 | 122 | refute_valid schema, data 123 | 124 | data['a'] = false 125 | 126 | assert_valid schema, data 127 | end 128 | 129 | def test_format_datetime 130 | schema = { 131 | 'type' => 'object', 132 | 'properties' => { 'a' => { 'type' => 'string', 'format' => 'date-time' } }, 133 | } 134 | 135 | assert_valid schema, { 'a' => '2010-01-01T12:00:00Z' } 136 | refute_valid schema, { 'a' => '2010-01-32T12:00:00Z' } 137 | refute_valid schema, { 'a' => '2010-13-01T12:00:00Z' } 138 | refute_valid schema, { 'a' => '2010-01-01T24:00:00Z' } 139 | refute_valid schema, { 'a' => '2010-01-01T12:60:00Z' } 140 | refute_valid schema, { 'a' => '2010-01-01T12:00:60Z' } 141 | refute_valid schema, { 'a' => '2010-01-01T12:00:00z' } 142 | refute_valid schema, { 'a' => '2010-01-0112:00:00Z' } 143 | refute_valid schema, { 'a' => "2010-01-01T12:00:00Z\nabc" } 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/json-schema/util/uri.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'addressable/uri' 4 | 5 | module JSON 6 | module Util 7 | # @api private 8 | class URI < Addressable::URI 9 | SUPPORTED_PROTOCOLS = %w[http https ftp tftp sftp ssh svn+ssh telnet nntp gopher wais ldap prospero] 10 | 11 | class << self 12 | alias unescape_uri unescape 13 | 14 | # @param uri [String, Addressable::URI] 15 | # @return [Addressable::URI, nil] 16 | def parse(uri) 17 | super 18 | rescue Addressable::URI::InvalidURIError => e 19 | raise JSON::Schema::UriError, e.message 20 | end 21 | 22 | # @param uri [String, Addressable::URI] 23 | # @return [Addressable::URI, nil] 24 | def file_uri(uri) 25 | convert_path(parse(uri).path) 26 | end 27 | 28 | # @param uri [String, Addressable::URI 29 | # @return [String] 30 | def unescaped_path(uri) 31 | parse(uri).unescaped_path 32 | end 33 | 34 | # Strips the fragment from the URI. 35 | # @param uri [String, Addressable::URI] 36 | # @return [Addressable::URI] 37 | def strip_fragment(uri) 38 | parse(uri).strip_fragment 39 | end 40 | 41 | # @param uri [String, Addressable::URI] 42 | # @return [Addressable::URI] 43 | def normalized_uri(uri, base_path = Dir.pwd) 44 | parse(uri).normalized_uri(base_path) 45 | end 46 | 47 | # Normalizes the reference URI based on the provided base URI 48 | # 49 | # @param ref [String, Addressable::URI] 50 | # @param base [String, Addressable::URI] 51 | # @return [Addressable::URI] 52 | def normalize_ref(ref, base) 53 | parse(ref).normalize_ref(base) 54 | end 55 | 56 | def absolutize_ref(ref, base) 57 | parse(ref).absolutize_ref(base) 58 | end 59 | end 60 | 61 | # Unencodes any percent encoded characters within a path component. 62 | # 63 | # @return [String] 64 | def unescaped_path 65 | self.class.unescape_component(path) 66 | end 67 | 68 | # Strips the fragment from the URI. 69 | # @return [Addressable::URI] a new instance of URI without a fragment 70 | def strip_fragment 71 | if fragment.nil? || fragment.empty? 72 | self 73 | else 74 | merge(fragment: '') 75 | end 76 | end 77 | 78 | # Normalizes the URI based on the provided base path. 79 | # 80 | # @param base_path [String] the base path to use for relative URIs. Defaults to the current working directory. 81 | # @return [Addressable::URI] the normalized URI or nil 82 | def normalized_uri(base_path = Dir.pwd) 83 | if relative? 84 | if path[0, 1] == '/' 85 | self.class.file_uri(self) 86 | else 87 | self.class.file_uri(File.join(base_path, self)) 88 | end 89 | else 90 | self 91 | end 92 | end 93 | 94 | # @param base [Addressable::URI, String] 95 | # @return [Addressable::URI] 96 | def normalize_ref(base) 97 | base_uri = self.class.parse(base) 98 | defer_validation do 99 | if relative? 100 | # Check for absolute path 101 | path, fragment = to_s.split('#') 102 | merge!(base_uri) 103 | 104 | if path.nil? || path == '' 105 | self.path = base_uri.path 106 | elsif path[0, 1] == '/' 107 | self.path = Pathname.new(path).cleanpath.to_s 108 | else 109 | join!(path) 110 | end 111 | 112 | self.fragment = fragment 113 | end 114 | 115 | self.fragment = '' if self.fragment.nil? || self.fragment.empty? 116 | end 117 | 118 | self 119 | end 120 | 121 | # @param base [Addressable::URI, String] 122 | # @return [Addressable::URI] 123 | def absolutize_ref(base) 124 | ref = strip_fragment 125 | if ref.absolute? 126 | ref 127 | else 128 | self.class.strip_fragment(base).join(ref.path).normalized_uri 129 | end 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /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' => %w[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-06/schema#", 3 | "$id": "http://json-schema.org/draft-06/schema#", 4 | "title": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "nonNegativeInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "nonNegativeIntegerDefault0": { 16 | "allOf": [ 17 | { "$ref": "#/definitions/nonNegativeInteger" }, 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 | "default": [] 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/nonNegativeInteger" }, 77 | "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 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/nonNegativeInteger" }, 91 | "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 92 | "uniqueItems": { 93 | "type": "boolean", 94 | "default": false 95 | }, 96 | "contains": { "$ref": "#" }, 97 | "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, 98 | "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 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 | 32 | def error_message 33 | "Read of #{type_string} at #{location} refused" 34 | end 35 | end 36 | 37 | # Raised by {JSON::Schema::Reader} when an attempt to read a schema fails 38 | class ReadFailed < ReadError 39 | private 40 | 41 | def error_message 42 | "Read of #{type_string} at #{location} failed" 43 | end 44 | end 45 | 46 | # When an unregistered schema is encountered, the {JSON::Schema::Reader} is 47 | # used to fetch its contents and register it with the {JSON::Validator}. 48 | # 49 | # This default reader will read schemas from the filesystem or from a URI. 50 | class Reader 51 | # The behavior of the schema reader can be controlled by providing 52 | # callbacks to determine whether to permit reading referenced schemas. 53 | # The options +accept_uri+ and +accept_file+ should be procs which 54 | # accept a +URI+ or +Pathname+ object, and return a boolean value 55 | # indicating whether to read the referenced schema. 56 | # 57 | # URIs using the +file+ scheme will be normalized into +Pathname+ objects 58 | # and passed to the +accept_file+ callback. 59 | # 60 | # @param options [Hash] 61 | # @option options [Boolean, #call] accept_uri (true) 62 | # @option options [Boolean, #call] accept_file (true) 63 | # 64 | # @example Reject all unregistered schemas 65 | # JSON::Validator.schema_reader = JSON::Schema::Reader.new( 66 | # :accept_uri => false, 67 | # :accept_file => false 68 | # ) 69 | # 70 | # @example Only permit URIs from certain hosts 71 | # JSON::Validator.schema_reader = JSON::Schema::Reader.new( 72 | # :accept_file => false, 73 | # :accept_uri => proc { |uri| ['mycompany.com', 'json-schema.org'].include?(uri.host) } 74 | # ) 75 | def initialize(options = {}) 76 | @accept_uri = options.fetch(:accept_uri, true) 77 | @accept_file = options.fetch(:accept_file, true) 78 | end 79 | 80 | # @param location [#to_s] The location from which to read the schema 81 | # @return [JSON::Schema] 82 | # @raise [JSON::Schema::ReadRefused] if +accept_uri+ or +accept_file+ 83 | # indicated the schema could not be read 84 | # @raise [JSON::Schema::ParseError] if the schema was not a valid JSON object 85 | # @raise [JSON::Schema::ReadFailed] if reading the location was acceptable but the 86 | # attempt to retrieve it failed 87 | def read(location) 88 | uri = JSON::Util::URI.parse(location.to_s) 89 | body = if uri.scheme.nil? || uri.scheme == 'file' 90 | uri = JSON::Util::URI.file_uri(uri) 91 | read_file(Pathname.new(uri.path).expand_path) 92 | else 93 | read_uri(uri) 94 | end 95 | 96 | JSON::Schema.new(JSON::Validator.parse(body), uri) 97 | end 98 | 99 | # @param uri [Addressable::URI] 100 | # @return [Boolean] 101 | def accept_uri?(uri) 102 | if @accept_uri.respond_to?(:call) 103 | @accept_uri.call(uri) 104 | else 105 | @accept_uri 106 | end 107 | end 108 | 109 | # @param pathname [Pathname] 110 | # @return [Boolean] 111 | def accept_file?(pathname) 112 | if @accept_file.respond_to?(:call) 113 | @accept_file.call(pathname) 114 | else 115 | @accept_file 116 | end 117 | end 118 | 119 | private 120 | 121 | def read_uri(uri) 122 | if accept_uri?(uri) 123 | URI.open(uri.to_s).read 124 | else 125 | raise JSON::Schema::ReadRefused.new(uri.to_s, :uri) 126 | end 127 | rescue OpenURI::HTTPError, SocketError 128 | raise JSON::Schema::ReadFailed.new(uri.to_s, :uri) 129 | end 130 | 131 | def read_file(pathname) 132 | if accept_file?(pathname) 133 | File.read(JSON::Util::URI.unescaped_path(pathname.to_s)) 134 | else 135 | raise JSON::Schema::ReadRefused.new(pathname.to_s, :file) 136 | end 137 | rescue Errno::ENOENT 138 | raise JSON::Schema::ReadFailed.new(pathname.to_s, :file) 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /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 | 13 | assert(!JSON::Validator.validate(schema, data, strict: true)) 14 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 15 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 16 | 17 | data = { 'b' => 'b' } 18 | 19 | assert(!JSON::Validator.validate(schema, data, strict: true)) 20 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 21 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 22 | 23 | data = { 'a' => 'a', 'b' => 'b' } 24 | 25 | assert(JSON::Validator.validate(schema, data, strict: true)) 26 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 27 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 28 | 29 | data = { 'a' => 'a', 'b' => 'b', 'c' => 'c' } 30 | 31 | assert(!JSON::Validator.validate(schema, data, strict: true)) 32 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 33 | assert(!JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 34 | end 35 | 36 | def test_strict_error_message 37 | schema = { type: 'object', properties: { a: { type: 'string' } } } 38 | data = { a: 'abc', b: 'abc' } 39 | errors = JSON::Validator.fully_validate(schema, data, strict: true) 40 | 41 | assert_match("The property '#/' contained undefined properties: 'b' in schema", errors[0]) 42 | end 43 | 44 | def test_strict_properties_additional_props 45 | schema = { 46 | '$schema' => 'http://json-schema.org/draft-04/schema#', 47 | 'properties' => { 48 | 'a' => { 'type' => 'string' }, 49 | 'b' => { 'type' => 'string' }, 50 | }, 51 | 'additionalProperties' => { 'type' => 'integer' }, 52 | } 53 | 54 | data = { 'a' => 'a' } 55 | 56 | assert(!JSON::Validator.validate(schema, data, strict: true)) 57 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 58 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 59 | 60 | data = { 'b' => 'b' } 61 | 62 | assert(!JSON::Validator.validate(schema, data, strict: true)) 63 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 64 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 65 | 66 | data = { 'a' => 'a', 'b' => 'b' } 67 | 68 | assert(JSON::Validator.validate(schema, data, strict: true)) 69 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 70 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 71 | 72 | data = { 'a' => 'a', 'b' => 'b', 'c' => 'c' } 73 | 74 | assert(!JSON::Validator.validate(schema, data, strict: true)) 75 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 76 | assert(!JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 77 | 78 | data = { 'a' => 'a', 'b' => 'b', 'c' => 3 } 79 | 80 | assert(JSON::Validator.validate(schema, data, strict: true)) 81 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 82 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 83 | end 84 | 85 | def test_strict_properties_pattern_props 86 | schema = { 87 | 'properties' => { 88 | 'a' => { 'type' => 'string' }, 89 | 'b' => { 'type' => 'string' }, 90 | }, 91 | 'patternProperties' => { '\\d+ taco' => { 'type' => 'integer' } }, 92 | } 93 | 94 | data = { 'a' => 'a' } 95 | 96 | assert(!JSON::Validator.validate(schema, data, strict: true)) 97 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 98 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 99 | 100 | data = { 'b' => 'b' } 101 | 102 | assert(!JSON::Validator.validate(schema, data, strict: true)) 103 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 104 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 105 | 106 | data = { 'a' => 'a', 'b' => 'b' } 107 | 108 | assert(JSON::Validator.validate(schema, data, strict: true)) 109 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 110 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 111 | 112 | data = { 'a' => 'a', 'b' => 'b', 'c' => 'c' } 113 | 114 | assert(!JSON::Validator.validate(schema, data, strict: true)) 115 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 116 | assert(!JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 117 | 118 | data = { 'a' => 'a', 'b' => 'b', 'c' => 3 } 119 | 120 | assert(!JSON::Validator.validate(schema, data, strict: true)) 121 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 122 | assert(!JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 123 | 124 | data = { 'a' => 'a', 'b' => 'b', '23 taco' => 3 } 125 | 126 | assert(JSON::Validator.validate(schema, data, strict: true)) 127 | assert(JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 128 | assert(JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 129 | 130 | data = { 'a' => 'a', 'b' => 'b', '23 taco' => 'cheese' } 131 | 132 | assert(!JSON::Validator.validate(schema, data, strict: true)) 133 | assert(!JSON::Validator.validate(schema, data, allPropertiesRequired: true)) 134 | assert(!JSON::Validator.validate(schema, data, noAdditionalProperties: true)) 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /test/schema_validation_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 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 | end 37 | 38 | def invalid_schema_v4 39 | { 40 | '$schema' => 'http://json-schema.org/draft-04/schema#', 41 | 'type' => 'object', 42 | 'required' => 'b', 43 | 'properties' => {}, 44 | } 45 | end 46 | 47 | def symbolized_schema 48 | { 49 | type: :object, 50 | required: %i[ 51 | id 52 | name 53 | real_name 54 | role 55 | website 56 | biography 57 | created_at 58 | demographic 59 | ], 60 | properties: { 61 | id: { 62 | type: [ 63 | :integer, 64 | ], 65 | }, 66 | name: { 67 | type: [ 68 | :string, 69 | ], 70 | }, 71 | real_name: { 72 | type: [ 73 | :string, 74 | ], 75 | }, 76 | role: { 77 | type: [ 78 | :string, 79 | ], 80 | }, 81 | website: { 82 | type: %i[ 83 | string 84 | null 85 | ], 86 | }, 87 | created_at: { 88 | type: [ 89 | :string, 90 | ], 91 | }, 92 | biography: { 93 | type: %i[ 94 | string 95 | null 96 | ], 97 | }, 98 | }, 99 | relationships: { 100 | demographic: { 101 | type: :object, 102 | required: %i[ 103 | id 104 | gender 105 | ], 106 | properties: { 107 | id: { 108 | type: [ 109 | :integer, 110 | ], 111 | }, 112 | gender: { 113 | type: [ 114 | :string, 115 | ], 116 | }, 117 | }, 118 | }, 119 | }, 120 | } 121 | end 122 | 123 | def test_draft03_validation 124 | data = { 'b' => { 'a' => 5 } } 125 | 126 | assert(JSON::Validator.validate(valid_schema_v3, data, validate_schema: true, version: :draft3)) 127 | assert(!JSON::Validator.validate(invalid_schema_v3, data, validate_schema: true, version: :draft3)) 128 | end 129 | 130 | def test_validate_just_schema_draft03 131 | errors = JSON::Validator.fully_validate_schema(valid_schema_v3, version: :draft3) 132 | 133 | assert_empty errors 134 | 135 | errors = JSON::Validator.fully_validate_schema(invalid_schema_v3, version: :draft3) 136 | 137 | assert_equal 1, errors.size 138 | assert_match(/the property .*required.*did not match/i, errors.first) 139 | end 140 | 141 | def test_draft04_validation 142 | data = { 'b' => { 'a' => 5 } } 143 | 144 | assert(JSON::Validator.validate(valid_schema_v4, data, validate_schema: true, version: :draft4)) 145 | assert(!JSON::Validator.validate(invalid_schema_v4, data, validate_schema: true, version: :draft4)) 146 | end 147 | 148 | def test_validate_just_schema_draft04 149 | errors = JSON::Validator.fully_validate_schema(valid_schema_v4, version: :draft4) 150 | 151 | assert_empty errors 152 | 153 | errors = JSON::Validator.fully_validate_schema(invalid_schema_v4, version: :draft4) 154 | 155 | assert_equal 1, errors.size 156 | assert_match(/the property .*required.*did not match/i, errors.first) 157 | end 158 | 159 | def test_validate_schema_3_without_version_option 160 | data = { 'b' => { 'a' => 5 } } 161 | 162 | assert(JSON::Validator.validate(valid_schema_v3, data, validate_schema: true)) 163 | assert(!JSON::Validator.validate(invalid_schema_v3, data, validate_schema: true)) 164 | end 165 | 166 | def test_schema_validation_from_different_directory 167 | Dir.mktmpdir do |tmpdir| 168 | Dir.chdir(tmpdir) do 169 | data = { 'b' => { 'a' => 5 } } 170 | 171 | assert(JSON::Validator.validate(valid_schema_v4, data, validate_schema: true, version: :draft4)) 172 | assert(!JSON::Validator.validate(invalid_schema_v4, data, validate_schema: true, version: :draft4)) 173 | end 174 | end 175 | end 176 | 177 | def test_validate_schema_with_symbol_keys 178 | data = { 179 | 'created_at' => '2014-01-25T00:58:33-08:00', 180 | 'id' => 8_517_194_300_913_402_149_003, 181 | 'name' => 'chelsey', 182 | 'real_name' => 'Mekhi Hegmann', 183 | 'website' => nil, 184 | 'role' => 'user', 185 | 'biography' => nil, 186 | 'demographic' => nil, 187 | } 188 | 189 | assert(JSON::Validator.validate!(symbolized_schema, data, validate_schema: true)) 190 | end 191 | 192 | def test_validate_schema_no_additional_properties 193 | errors = JSON::Validator.fully_validate_schema(symbolized_schema, noAdditionalProperties: true) 194 | 195 | assert_equal 1, errors.size 196 | assert_match(/the property .* contained undefined properties: .*relationships/i, errors.first) 197 | 198 | schema_without_additional_properties = symbolized_schema 199 | schema_without_additional_properties.delete(:relationships) 200 | errors = JSON::Validator.fully_validate_schema(schema_without_additional_properties, noAdditionalProperties: true) 201 | 202 | assert_equal 0, errors.size 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /test/custom_format_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('support/test_helper', __dir__) 2 | 3 | class CustomFormatTest < Minitest::Test 4 | def setup 5 | @all_versions = ['draft1', 'draft2', 'draft3', 'draft4', 'draft6', nil] 6 | @format_proc = ->(value) { raise JSON::Schema::CustomFormatError, 'must be 42' unless value == '42' } 7 | @schema_6 = { 8 | '$schema' => 'http://json-schema.org/draft-06/schema#', 9 | 'properties' => { 10 | 'a' => { 11 | 'type' => 'string', 12 | 'format' => 'custom', 13 | }, 14 | }, 15 | } 16 | @schema_4 = @schema_6.clone 17 | @schema_4['$schema'] = 'http://json-schema.org/draft-04/schema#' 18 | @schema_3 = @schema_6.clone 19 | @schema_3['$schema'] = 'http://json-schema.org/draft-03/schema#' 20 | @schema_2 = @schema_6.clone 21 | @schema_2['$schema'] = 'http://json-schema.org/draft-02/schema#' 22 | @schema_1 = @schema_6.clone 23 | @schema_1['$schema'] = 'http://json-schema.org/draft-01/schema#' 24 | @default = @schema_6.clone 25 | @default.delete('$schema') 26 | @schemas = { 27 | 'draft1' => @schema_1, 28 | 'draft2' => @schema_2, 29 | 'draft3' => @schema_3, 30 | 'draft4' => @schema_4, 31 | 'draft6' => @schema_6, 32 | nil => @default, 33 | } 34 | JSON::Validator.restore_default_formats 35 | end 36 | 37 | def test_single_registration 38 | @all_versions.each do |version| 39 | assert_nil(JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' for #{version || 'default'} should be nil") 40 | JSON::Validator.register_format_validator('custom', @format_proc, [version]) 41 | 42 | assert_kind_of(JSON::Schema::CustomFormat, JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' should be registered for #{version || 'default'}") 43 | (@all_versions - [version]).each do |other_version| 44 | assert_nil(JSON::Validator.validator_for_name(other_version).formats['custom'], "Format 'custom' should still be nil for #{other_version || 'default'}") 45 | end 46 | JSON::Validator.deregister_format_validator('custom', [version]) 47 | 48 | assert_nil(JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' should be deregistered for #{version || 'default'}") 49 | end 50 | end 51 | 52 | def test_register_for_all_by_default 53 | JSON::Validator.register_format_validator('custom', @format_proc) 54 | 55 | @all_versions.each do |version| 56 | assert_kind_of(JSON::Schema::CustomFormat, JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' should be registered for #{version || 'default'}") 57 | end 58 | JSON::Validator.restore_default_formats 59 | 60 | @all_versions.each do |version| 61 | assert_nil(JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' should still be nil for #{version || 'default'}") 62 | end 63 | end 64 | 65 | def test_multi_registration 66 | unregistered_version = @all_versions.delete('draft1') 67 | JSON::Validator.register_format_validator('custom', @format_proc, @all_versions) 68 | 69 | @all_versions.each do |version| 70 | assert_kind_of(JSON::Schema::CustomFormat, JSON::Validator.validator_for_name(version).formats['custom'], "Format 'custom' should be registered for #{version || 'default'}") 71 | end 72 | assert_nil(JSON::Validator.validator_for_name(unregistered_version).formats['custom'], "Format 'custom' should still be nil for #{unregistered_version}") 73 | end 74 | 75 | def test_format_validation 76 | @all_versions.each do |version| 77 | data = { 78 | 'a' => '23', 79 | } 80 | schema = @schemas[version] 81 | prefix = "Validation for '#{version || 'default'}'" 82 | 83 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with no 'custom' format validator registered") 84 | 85 | JSON::Validator.register_format_validator('custom', @format_proc, [version]) 86 | data['a'] = '42' 87 | 88 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with 'custom' format validator and correct data") 89 | 90 | data['a'] = '23' 91 | 92 | assert(!JSON::Validator.validate(schema, data), "#{prefix} fails with 'custom' format validator and wrong data") 93 | 94 | errors = JSON::Validator.fully_validate(schema, data) 95 | 96 | assert_equal(1, errors.count) 97 | assert_match(%r{The property '#/a' must be 42 in schema}, errors.first, "#{prefix} records format error") 98 | 99 | errors_as_objects = JSON::Validator.fully_validate(schema, data, errors_as_objects: true) 100 | 101 | assert_match('CustomFormat', errors_as_objects.first[:failed_attribute], "#{prefix} tags failed attribute") 102 | 103 | data['a'] = 23 104 | errors = JSON::Validator.fully_validate(schema, data) 105 | 106 | assert_equal(1, errors.count) 107 | assert_match(%r{The property '#/a' of type integer did not match the following type: string}i, errors.first, "#{prefix} records no format error on type mismatch") 108 | end 109 | end 110 | 111 | def test_override_default_format 112 | @all_versions.each do |version| 113 | data = { 114 | 'a' => '2001:db8:85a3:0:0:8a2e:370:7334', 115 | } 116 | schema = @schemas[version] 117 | schema['properties']['a']['format'] = 'ipv6' 118 | prefix = "Validation for '#{version || 'default'}'" 119 | 120 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds for default format with correct data") 121 | 122 | data['a'] = 'no_ip6_address' 123 | 124 | assert(!JSON::Validator.validate(schema, data), "#{prefix} fails for default format and wrong data") 125 | 126 | data['a'] = '42' 127 | JSON::Validator.register_format_validator('ipv6', @format_proc, [version]) 128 | 129 | assert(JSON::Validator.validate(schema, data), "#{prefix} succeeds with overridden default format and correct data") 130 | 131 | JSON::Validator.deregister_format_validator('ipv6', [version]) 132 | data['a'] = '2001:db8:85a3:0:0:8a2e:370:7334' 133 | 134 | assert(JSON::Validator.validate(schema, data), "#{prefix} restores the default format on deregistration") 135 | end 136 | end 137 | end 138 | --------------------------------------------------------------------------------