diff --git a/README.md b/README.md index 726a5b83..2dd7a669 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,18 @@ JSON::Validator.validate(schema, { "a" => 1 }, :parse_data => false) # => false JSON::Validator.validate(schema, '{ "a": 1 }', :parse_data => false) +# +# with the `:nullable` option set to true, any key with null value will be evaluated to true +# +schema = { + "type": "object", + "properties": { + "github_url": { "type": "string", "nullable": true } + } +} +# => true +JSON::Validator.validate!(schema, { "github_url" => null }) + # # with the `:parse_integer` option set to false, the integer value given as string will not be parsed. # diff --git a/lib/json-schema/attribute.rb b/lib/json-schema/attribute.rb index 31eb98f7..29e1e572 100644 --- a/lib/json-schema/attribute.rb +++ b/lib/json-schema/attribute.rb @@ -33,8 +33,10 @@ def self.validation_errors(validator) 'any' => Object, } - def self.data_valid_for_type?(data, type) + def self.data_valid_for_type?(data, type, nullable: false) valid_classes = TYPE_CLASS_MAPPINGS.fetch(type) { return true } + valid_classes = [valid_classes, NilClass] if nullable + valid_classes.flatten! if valid_classes.is_a?(Array) Array(valid_classes).any? { |c| data.is_a?(c) } end diff --git a/lib/json-schema/attributes/type.rb b/lib/json-schema/attributes/type.rb index 149ac6af..76792408 100644 --- a/lib/json-schema/attributes/type.rb +++ b/lib/json-schema/attributes/type.rb @@ -5,10 +5,12 @@ class Schema class TypeAttribute < Attribute def self.validate(current_schema, data, fragments, processor, validator, options = {}) union = true + nullable = false if options[:disallow] types = current_schema.schema['disallow'] else types = current_schema.schema['type'] + nullable = current_schema.schema['nullable'] end if !types.is_a?(Array) @@ -22,7 +24,7 @@ def self.validate(current_schema, data, fragments, processor, validator, options types.each_with_index do |type, type_index| if type.is_a?(String) - valid = data_valid_for_type?(data, type) + valid = data_valid_for_type?(data, type, nullable: nullable) elsif type.is_a?(Hash) && union # Validate as a schema schema = JSON::Schema.new(type, current_schema.uri, validator) diff --git a/lib/json-schema/attributes/type_v4.rb b/lib/json-schema/attributes/type_v4.rb index f5d5a3a7..cc85186d 100644 --- a/lib/json-schema/attributes/type_v4.rb +++ b/lib/json-schema/attributes/type_v4.rb @@ -6,12 +6,13 @@ class TypeV4Attribute < Attribute def self.validate(current_schema, data, fragments, processor, validator, options = {}) union = true types = current_schema.schema['type'] + nullable = current_schema.schema['nullable'] if !types.is_a?(Array) types = [types] union = false end - return if types.any? { |type| data_valid_for_type?(data, type) } + return if types.any? { |type| data_valid_for_type?(data, type, nullable: nullable) } types = types.map { |type| type.is_a?(String) ? type : '(schema)' }.join(', ') message = format( diff --git a/test/initialize_data_test.rb b/test/initialize_data_test.rb index 9a531258..ba1662bf 100644 --- a/test/initialize_data_test.rb +++ b/test/initialize_data_test.rb @@ -379,4 +379,15 @@ def test_parse_hash_with_instantiated_validator assert_raises(TypeError) { v.validate(data) } assert_raises(TypeError) { v.validate(data) } end + + def test_nullable_param + schema = { 'type' => 'object', 'properties' => { 'a' => { 'type' => 'string', 'nullable' => true } } } + data = { 'a' => nil } + + assert(JSON::Validator.validate(schema, data)) + + schema = { 'type' => 'object', 'properties' => { 'a' => { 'type' => 'string' } } } + + refute(JSON::Validator.validate(schema, data)) + end end