From 5c002fe696f848f068e51a4a0c7bfbbad43d6876 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Tue, 26 Sep 2023 12:51:16 -0700 Subject: [PATCH 01/18] 2.1.0 changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b36c7f..a9d2b37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.1.0] - XXXX-XX-XX + +[2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0 + ## [2.0.0] - 2023-08-20 For 2.0.0, much of the codebase was rewritten to simplify support for the two new JSON Schema draft versions (2019-09 and 2020-12). The major change is moving each keyword into its own class and organizing them into vocabularies. [Output formats](https://json-schema.org/draft/2020-12/json-schema-core.html#section-12) and [annotations](https://json-schema.org/draft/2020-12/json-schema-core.html#section-7.7) from the new drafts are also supported. The known breaking changes are listed below, but there may be others that haven't been identified. From be45f04e515df7da9eb0d9694bf3b1db3a7fbfdd Mon Sep 17 00:00:00 2001 From: David Harsha Date: Sun, 24 Sep 2023 14:07:49 -0700 Subject: [PATCH 02/18] Require discriminator `propertyName` property https://spec.openapis.org/oas/v3.1.0#discriminator-object > The expectation now is that a property with name petType MUST be present in the response payload It's a little unclear because the specification examples mostly specify the `propertyName` property under `required` as well, which seems redundant. It's probably safer this way, though, and the spec says "MUST" so we must. --- CHANGELOG.md | 4 ++++ lib/json_schemer/openapi31/vocab/base.rb | 3 ++- test/open_api_test.rb | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d2b37c..d66d6f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [2.1.0] - XXXX-XX-XX +### Bug Fixes + +- Require discriminator `propertyName` property + [2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0 ## [2.0.0] - 2023-08-20 diff --git a/lib/json_schemer/openapi31/vocab/base.rb b/lib/json_schemer/openapi31/vocab/base.rb index 937fbcfc..dae66117 100644 --- a/lib/json_schemer/openapi31/vocab/base.rb +++ b/lib/json_schemer/openapi31/vocab/base.rb @@ -46,7 +46,8 @@ def validate(instance, instance_location, keyword_location, context) property_name = value.fetch('propertyName') mapping = value['mapping'] || {} - return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) && instance.key?(property_name) + return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) + return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name) property = instance.fetch(property_name) ref = mapping.fetch(property, property) diff --git a/test/open_api_test.rb b/test/open_api_test.rb index 14423441..c24f2327 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -205,7 +205,7 @@ def test_discriminator_specification_example assert_equal([['enum', '/components/schemas/Cat/allOf/1/properties/huntingSkill']], schemer.validate(invalid_hunting_skill).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Dog/allOf/1']], schemer.validate(missing_pack_size).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['format', '/components/schemas/Dog/allOf/1/properties/packSize']], schemer.validate(invalid_pack_size).map { |error| error.values_at('type', 'schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet']], schemer.validate(missing_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet']], schemer.validate(missing_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Pet'], ['required', '/components/schemas/Cat/allOf/1']], schemer.validate(missing_name).map { |error| error.values_at('type', 'schema_pointer') }) assert_raises(JSONSchemer::UnknownRef) { schemer.validate(invalid_pet_type) } end @@ -404,7 +404,7 @@ def test_all_of_discriminator_subclass_schemas_work_on_their_own assert(schemer.valid?(CAT)) assert(schemer.valid?(MISTY)) assert_equal(['/components/schemas/Cat/allOf/1/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) end def test_all_of_discriminator_with_non_discriminator_ref @@ -444,7 +444,7 @@ def test_all_of_discriminator_with_non_discriminator_ref refute(schemer.valid?(CAT)) assert(schemer.valid?(CAT.merge('other' => 'y'))) assert_equal(['/components/schemas/Other', '/components/schemas/Cat/allOf/2/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert_equal([['required', '/components/schemas/Pet'], ['required', '/components/schemas/Other']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) + assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet'], ['required', '/components/schemas/Other']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) end def test_any_of_discriminator_without_matching_schema @@ -597,7 +597,7 @@ def test_non_json_pointer_discriminator def test_discriminator_non_object_and_missing_property_name schemer = JSONSchemer.schema({ 'discriminator' => { 'propertyName' => 'x' } }, :meta_schema => JSONSchemer.openapi31) assert(schemer.valid?(1)) - assert(schemer.valid?({ 'y' => 'z' })) + refute(schemer.valid?({ 'y' => 'z' })) end def test_openapi31_formats From e5ca2b1ef4f3d63b1ea09064706b195132c25155 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Mon, 25 Sep 2023 15:14:06 -0700 Subject: [PATCH 03/18] Support `Schema#ref` in subschemas `resolve_ref` needs to be called on the root schema because that's where all the `resources` are stored. The behavior here is kind of confusing because the ref is resolved just like the `$ref` keyword would be in the schema, so it's dependent on the schema's base URI. That means for a subschema `ref('#')` doesn't necessarily resolve to itself (ie, if the subschema doesn't have `$id`). --- CHANGELOG.md | 1 + lib/json_schemer/schema.rb | 2 +- test/json_schemer_test.rb | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d66d6f53..4b9f08ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fixes - Require discriminator `propertyName` property +- Support `Schema#ref` in subschemas [2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0 diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index db68a4af..bdce6b8c 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -113,7 +113,7 @@ def validate_schema end def ref(value) - resolve_ref(URI.join(base_uri, value)) + root.resolve_ref(URI.join(base_uri, value)) end def validate_instance(instance, instance_location, keyword_location, context) diff --git a/test/json_schemer_test.rb b/test/json_schemer_test.rb index 1bde86f3..bb5c0521 100644 --- a/test/json_schemer_test.rb +++ b/test/json_schemer_test.rb @@ -380,6 +380,12 @@ def test_schema_ref 'type' => 'integer', '$defs' => { 'foo' => { + '$id' => 'subschemer', + '$defs' => { + 'bar' => { + 'required' => ['z'] + } + }, 'type' => 'object', 'required' => ['x', 'y'], 'properties' => { @@ -401,10 +407,20 @@ def test_schema_ref refute(subschemer.valid?(1)) assert_equal( - [["/x", "/$defs/foo/properties/x", "string"], ["", "/$defs/foo", "required"]], + [['/x', '/$defs/foo/properties/x', 'string'], ['', '/$defs/foo', 'required']], subschemer.validate({ 'x' => 1 }).map { |error| error.values_at('data_pointer', 'schema_pointer', 'type') } ) assert(subschemer.valid?({ 'x' => '1', 'y' => 1 })) + + subsubschemer = subschemer.ref('#/$defs/bar') + refute(subsubschemer.valid?({ 'x' => 1 })) + assert_equal( + [['', '/$defs/foo/$defs/bar', 'required']], + subsubschemer.validate({ 'x' => 1 }).map { |error| error.values_at('data_pointer', 'schema_pointer', 'type') } + ) + + assert_equal(subschemer, subschemer.ref('#')) + assert_equal(subschemer, subsubschemer.ref('#')) end def test_published_meta_schemas From d5d5900a252ec9d4f82292b7bd4469f8be44ca30 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Mon, 25 Sep 2023 18:20:21 -0700 Subject: [PATCH 04/18] Limit anyOf/oneOf discriminator to listed refs This addresses an issue where `resolve_ref` gets called for refs that aren't explicitly listed in `anyOf` or `oneOf`. The [specification][0] says: > In both the oneOf and anyOf use cases, all possible schemas MUST be listed explicitly. So for anyOf/oneOf discriminators, this uses the provided refs to build a mapping of property values to schema objects for lookup during validation. Explicit mappings are found by schema name or full ref to support: ```yaml discriminator: propertyName: petType mapping: cat: Cat dog: '#/components/schemas/Dog' ``` Impicit mappings are only created for refs under `#/components/schemas/` and are overridden by any explicit mappings that point to the same schema. `allOf` discriminators still resolve refs because there isn't an explicit list of allowed refs. Switched to `Schema#ref` now that it calls `root.resolve_ref` itself. `FIXED_FIELD_REGEX` comes from the [spec][1]: > All the fixed fields declared above are objects that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$. It's used in all cases to make sure schemas are only looked up by name if the property value is a valid schema name. Closes: https://github.com/davishmcclurg/json_schemer/issues/144 [0]: https://spec.openapis.org/oas/v3.1.0#discriminator-object [1]: https://spec.openapis.org/oas/v3.1.0#components-object --- CHANGELOG.md | 1 + lib/json_schemer/openapi31/vocab/base.rb | 93 +++++++++++++------ test/open_api_test.rb | 108 ++++++++++++++++++++++- 3 files changed, 170 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9f08ef..af29cf55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug Fixes +- Limit anyOf/oneOf discriminator to listed refs - Require discriminator `propertyName` property - Support `Schema#ref` in subschemas diff --git a/lib/json_schemer/openapi31/vocab/base.rb b/lib/json_schemer/openapi31/vocab/base.rb index dae66117..7a59c5b3 100644 --- a/lib/json_schemer/openapi31/vocab/base.rb +++ b/lib/json_schemer/openapi31/vocab/base.rb @@ -34,7 +34,8 @@ def validate(*) end class Discriminator < Keyword - include Format::JSONPointer + # https://spec.openapis.org/oas/v3.1.0#components-object + FIXED_FIELD_REGEX = /\A[a-zA-Z0-9\.\-_]+$\z/ attr_accessor :skip_ref_once @@ -42,44 +43,80 @@ def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match `discriminator` schema" end - def validate(instance, instance_location, keyword_location, context) - property_name = value.fetch('propertyName') - mapping = value['mapping'] || {} + def mapping + @mapping ||= value['mapping'] || {} + end - return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) - return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name) + def subschemas_by_property_value + @subschemas_by_property_value ||= if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf') + subschemas = schema.parsed['anyOf']&.parsed || [] + subschemas += schema.parsed['oneOf']&.parsed || [] - property = instance.fetch(property_name) - ref = mapping.fetch(property, property) + subschemas_by_ref = {} + subschemas_by_schema_name = {} + + subschemas.each do |subschema| + subschema_ref = subschema.parsed.fetch('$ref').parsed + subschemas_by_ref[subschema_ref] = subschema - ref_schema = nil - unless ref.start_with?('#') && valid_json_pointer?(ref.delete_prefix('#')) - ref_schema = begin - root.resolve_ref(URI.join(schema.base_uri, "#/components/schemas/#{ref}")) - rescue InvalidRefPointer - nil + if subschema_ref.start_with?('#/components/schemas/') + schema_name = subschema_ref.delete_prefix('#/components/schemas/') + subschemas_by_schema_name[schema_name] = subschema if FIXED_FIELD_REGEX.match?(schema_name) + end end - end - ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref)) - return if skip_ref_once == ref_schema.absolute_keyword_location + explicit_mapping = mapping.transform_values do |schema_name_or_ref| + subschemas_by_schema_name.fetch(schema_name_or_ref) { subschemas_by_ref.fetch(schema_name_or_ref) } + end - nested = [] + implicit_mapping = subschemas_by_schema_name.reject do |_schema_name, subschema| + explicit_mapping.value?(subschema) + end - if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf') - subschemas = schema.parsed['anyOf']&.parsed || [] - subschemas += schema.parsed['oneOf']&.parsed || [] - subschemas.each do |subschema| - if subschema.parsed.fetch('$ref').ref_schema.absolute_keyword_location == ref_schema.absolute_keyword_location - nested << subschema.validate_instance(instance, instance_location, keyword_location, context) + implicit_mapping.merge(explicit_mapping) + else + Hash.new do |hash, property_value| + schema_name_or_ref = mapping.fetch(property_value, property_value) + + subschema = nil + + if FIXED_FIELD_REGEX.match?(schema_name_or_ref) + subschema = begin + schema.ref("#/components/schemas/#{schema_name_or_ref}") + rescue InvalidRefPointer + nil + end end + + subschema ||= begin + schema.ref(schema_name_or_ref) + rescue InvalidRefResolution, UnknownRef + nil + end + + hash[property_value] = subschema end - else - ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location - nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context) end + end + + def validate(instance, instance_location, keyword_location, context) + return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) + + property_name = value.fetch('propertyName') + + return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name) + + property_value = instance.fetch(property_name) + subschema = subschemas_by_property_value[property_value] + + return result(instance, instance_location, keyword_location, false) unless subschema + + return if skip_ref_once == subschema.absolute_keyword_location + subschema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location + + subschema_result = subschema.validate_instance(instance, instance_location, keyword_location, context) - result(instance, instance_location, keyword_location, (nested.any? && nested.all?(&:valid)), nested) + result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested) ensure self.skip_ref_once = nil end diff --git a/test/open_api_test.rb b/test/open_api_test.rb index c24f2327..72ff2fbb 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -207,7 +207,7 @@ def test_discriminator_specification_example assert_equal([['format', '/components/schemas/Dog/allOf/1/properties/packSize']], schemer.validate(invalid_pack_size).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet']], schemer.validate(missing_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) assert_equal([['required', '/components/schemas/Pet'], ['required', '/components/schemas/Cat/allOf/1']], schemer.validate(missing_name).map { |error| error.values_at('type', 'schema_pointer') }) - assert_raises(JSONSchemer::UnknownRef) { schemer.validate(invalid_pet_type) } + assert_equal([['discriminator', '/components/schemas/Pet']], schemer.validate(invalid_pet_type).map { |error| error.values_at('type', 'schema_pointer') }) end def test_all_of_discriminator @@ -447,6 +447,51 @@ def test_all_of_discriminator_with_non_discriminator_ref assert_equal([['required', '/components/schemas/Pet'], ['discriminator', '/components/schemas/Pet'], ['required', '/components/schemas/Other']], schemer.validate({}).map { |error| error.values_at('type', 'schema_pointer') }) end + def test_all_of_discriminator_with_remote_ref + schema = { + '$id' => 'http://example.com/schema', + 'discriminator' => { + 'propertyName' => 'petType', + 'mapping' => { + 'Dog' => 'http://example.com/dog' + } + } + } + schemer = JSONSchemer.schema( + schema, + :meta_schema => JSONSchemer.openapi31, + :ref_resolver => { + URI('http://example.com/schema') => schema, + URI('http://example.com/cat') => { + 'allOf' => [ + { '$ref' => 'http://example.com/schema' }, + CAT_SCHEMA + ] + }, + URI('http://example.com/dog') => { + 'allOf' => [ + { '$ref' => 'http://example.com/schema' }, + DOG_SCHEMA + ] + } + }.to_proc + ) + + assert(schemer.valid_schema?) + refute(schemer.valid?(CAT)) + assert(schemer.valid?(CAT.merge('petType' => 'http://example.com/cat'))) + assert(schemer.valid?(DOG)) + + invalid_cat = INVALID_CAT.merge('petType' => 'http://example.com/cat') + invalid_cat_result = schemer.validate(invalid_cat, output_format: 'basic', resolve_enumerators: true) + assert_equal('/discriminator/allOf/1/properties/name/type', invalid_cat_result.dig('errors', 0, 'keywordLocation')) + assert_equal('http://example.com/cat#/allOf/1/properties/name/type', invalid_cat_result.dig('errors', 0, 'absoluteKeywordLocation')) + + invalid_dog_result = schemer.validate(INVALID_DOG, output_format: 'basic', resolve_enumerators: true) + assert_equal('/discriminator/allOf/1/properties/bark/type', invalid_dog_result.dig('errors', 0, 'keywordLocation')) + assert_equal('http://example.com/dog#/allOf/1/properties/bark/type', invalid_dog_result.dig('errors', 0, 'absoluteKeywordLocation')) + end + def test_any_of_discriminator_without_matching_schema openapi = { 'openapi' => '3.1.0', @@ -513,6 +558,60 @@ def test_one_of_discriminator_without_matching_schema assert_equal([['discriminator', '/components/schemas/MyResponseType']], schemer.validate(INVALID_LIZARD).map { |error| error.values_at('type', 'schema_pointer') }) end + def test_any_of_discriminator_ignores_nested_schemas + openapi = { + 'openapi' => '3.1.0', + 'components' => { + 'schemas' => { + 'MyResponseType' => { + 'anyOf' => [ + { '$ref' => '#/components/schemas/Cat' }, + { '$ref' => '#/components/schemas/Cat/$defs/nah' } + ], + 'discriminator' => { + 'propertyName' => 'petType' + } + }, + 'Cat' => CAT_SCHEMA.merge('$defs' => { 'nah' => {} }) + } + } + } + + schemer = JSONSchemer.openapi(openapi).schema('MyResponseType') + + assert(schemer.valid_schema?) + assert(schemer.valid?(CAT)) + refute(schemer.valid?(CAT.merge('petType' => 'nah'))) + refute(schemer.valid?(CAT.merge('petType' => 'Cat/$defs/nah'))) + end + + def test_one_of_discriminator_ignores_nested_schemas + openapi = { + 'openapi' => '3.1.0', + 'components' => { + 'schemas' => { + 'MyResponseType' => { + 'oneOf' => [ + { '$ref' => '#/components/schemas/Cat' }, + { '$ref' => '#/components/schemas/Cat/$defs/nah' } + ], + 'discriminator' => { + 'propertyName' => 'petType' + } + }, + 'Cat' => CAT_SCHEMA.merge('$defs' => { 'nah' => {} }) + } + } + } + + schemer = JSONSchemer.openapi(openapi).schema('MyResponseType') + + assert(schemer.valid_schema?) + assert(schemer.valid?(CAT)) + refute(schemer.valid?(CAT.merge('petType' => 'nah'))) + refute(schemer.valid?(CAT.merge('petType' => 'Cat/$defs/nah'))) + end + def test_discrimator_mapping openapi = { 'openapi' => '3.1.0', @@ -542,7 +641,7 @@ def test_discrimator_mapping assert(schemer.valid_schema?) assert(schemer.valid?(CAT.merge('petType' => 'c'))) - assert(schemer.valid?(MISTY.merge('petType' => 'Cat'))) + refute(schemer.valid?(MISTY.merge('petType' => 'Cat'))) assert_equal(['/components/schemas/Cat/properties/name'], schemer.validate(INVALID_CAT.merge('petType' => 'c')).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(DOG.merge('petType' => 'd'))) assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG.merge('petType' => 'dog')).map { |error| error.fetch('schema_pointer') }) @@ -585,8 +684,9 @@ def test_non_json_pointer_discriminator assert(schemer.valid?(CAT)) assert(schemer.valid?(MISTY)) assert_equal(['/components/schemas/Cat/properties/name'], schemer.validate(INVALID_CAT).map { |error| error.fetch('schema_pointer') }) - assert(schemer.valid?(DOG)) - assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG).map { |error| error.fetch('schema_pointer') }) + refute(schemer.valid?(DOG)) + assert_equal(['/components/schemas/MyResponseType'], schemer.validate(INVALID_DOG).map { |error| error.fetch('schema_pointer') }) + assert_equal(['/components/schemas/Dog/properties/bark'], schemer.validate(INVALID_DOG.merge('petType' => 'dog')).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(LIZARD)) assert_equal(['/components/schemas/Lizard/properties/lovesRocks'], schemer.validate(INVALID_LIZARD).map { |error| error.fetch('schema_pointer') }) assert(schemer.valid?(MONSTER)) From 4fafc60303cb27dbf903c88aae8806f1d1b6aeed Mon Sep 17 00:00:00 2001 From: David Harsha Date: Fri, 13 Oct 2023 14:29:23 -0700 Subject: [PATCH 05/18] Follow meta schema redirects in tests It looks like they changed these to redirects: ``` % curl 'http://json-schema.org/draft-07/schema#' -I HTTP/1.1 301 Moved Permanently Date: Fri, 13 Oct 2023 21:29:36 GMT Connection: keep-alive Cache-Control: max-age=3600 Expires: Fri, 13 Oct 2023 22:29:36 GMT Location: https://json-schema.org/draft-07/schema Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=4Y8wC8Dh8QkEwNGjF%2FFKlVkfEiFB1hOv3XOnfGJPcIKfna%2FrsYwkURU1zkA5HAgGXRsXFZ3EUCjMOnZiLkXNjDJQ3zvaJJvsS43oAUhIM0uu2CFTfa4QHGRWI%2FpXcULLCqXrAzuUu%2FdG3RU0tFU%3D"}],"group":"cf-nel","max_age":604800} NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800} Vary: Accept-Encoding CF-Cache-Status: DYNAMIC Server: cloudflare CF-RAY: 815aadd5892dcf05-SJC ``` Seems wrong to me, but idk. Added a test helper to resolve redirects when fetching things. --- test/json_schema_test_suite_test.rb | 2 +- test/json_schemer_test.rb | 2 +- test/test_helper.rb | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/json_schema_test_suite_test.rb b/test/json_schema_test_suite_test.rb index 26dedbef..f68d1a60 100644 --- a/test/json_schema_test_suite_test.rb +++ b/test/json_schema_test_suite_test.rb @@ -45,7 +45,7 @@ class JSONSchemaTestSuiteTest < Minitest::Test path = Pathname.new(__dir__).join('..', 'JSON-Schema-Test-Suite', 'remotes', uri.path.gsub(/\A\//, '')) JSON.parse(path.read) else - JSON.parse(Net::HTTP.get(uri)) + JSON.parse(fetch(uri)) end end diff --git a/test/json_schemer_test.rb b/test/json_schemer_test.rb index 1bde86f3..192f8b8e 100644 --- a/test/json_schemer_test.rb +++ b/test/json_schemer_test.rb @@ -422,7 +422,7 @@ def test_published_meta_schemas JSONSchemer::OpenAPI30::Document::SCHEMA ].each do |meta_schema| id = meta_schema.key?('$id') ? meta_schema.fetch('$id') : meta_schema.fetch('id') - assert_equal(meta_schema, JSON.parse(Net::HTTP.get(URI(id)))) + assert_equal(meta_schema, JSON.parse(fetch(id))) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 60544faa..8a576cb2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,3 +12,17 @@ require "json_schemer" require "minitest/autorun" + +def fetch(location, limit = 10) + raise if limit.zero? + uri = URI(location) + response = Net::HTTP.get_response(uri) + case response + when Net::HTTPSuccess + response.body + when Net::HTTPRedirection + fetch(URI.join(uri, response['Location']), limit - 1) + else + response.value.body + end +end From e73f9feeeb86539b8f085d0095ea94873e99c44f Mon Sep 17 00:00:00 2001 From: David Harsha Date: Fri, 13 Oct 2023 14:33:01 -0700 Subject: [PATCH 06/18] Use default base URI for exclusive refs In drafts 7 and earlier, all keywords except `definitions` are ignored when `$ref` is present. This includes `$id`, which means refs are resolved using the schema's inherited base URI (default or parent). Currently, when `$id` is present, the inherited base URI isn't being registered as a ref resolution resource (with `ID_KEYWORD_CLASS.new`), which causes JSON pointer ref resolution to fail. The fix here is to add the exclusive ref case for the `ID_KEYWORD_CLASS.new` call. The rest of the changes are from if/else cleanup. Closes: https://github.com/davishmcclurg/json_schemer/issues/146 --- lib/json_schemer/schema.rb | 37 ++++++++++++++++++------------------- test/ref_test.rb | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index db68a4af..1a96d5ba 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -329,28 +329,27 @@ def parse VOCABULARY_KEYWORD_CLASS.new(vocabulary, self, '$vocabulary') end - if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword)) + keywords = meta_schema.keywords + exclusive_ref = value.is_a?(Hash) && value.key?('$ref') && keywords.fetch('$ref').exclusive? + + if root == self && (!value.is_a?(Hash) || !value.key?(meta_schema.id_keyword) || exclusive_ref) ID_KEYWORD_CLASS.new(base_uri, self, meta_schema.id_keyword) end - if value.is_a?(Hash) - keywords = meta_schema.keywords - - if value.key?('$ref') && keywords.fetch('$ref').exclusive? - @parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref') - defs_keyword = meta_schema.defs_keyword - if value.key?(defs_keyword) && keywords.key?(defs_keyword) - @parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword) - end - else - keyword_order = meta_schema.keyword_order - last = keywords.size - - value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)| - keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last) - end.each do |keyword, value| - @parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword) - end + if exclusive_ref + @parsed['$ref'] = keywords.fetch('$ref').new(value.fetch('$ref'), self, '$ref') + defs_keyword = meta_schema.defs_keyword + if value.key?(defs_keyword) && keywords.key?(defs_keyword) + @parsed[defs_keyword] = keywords.fetch(defs_keyword).new(value.fetch(defs_keyword), self, defs_keyword) + end + elsif value.is_a?(Hash) + keyword_order = meta_schema.keyword_order + last = keywords.size + + value.sort do |(keyword_a, _value_a), (keyword_b, _value_b)| + keyword_order.fetch(keyword_a, last) <=> keyword_order.fetch(keyword_b, last) + end.each do |keyword, value| + @parsed[keyword] ||= keywords.fetch(keyword, UNKNOWN_KEYWORD_CLASS).new(value, self, keyword) end end diff --git a/test/ref_test.rb b/test/ref_test.rb index d9f812a5..1cf8edef 100644 --- a/test/ref_test.rb +++ b/test/ref_test.rb @@ -379,4 +379,20 @@ def test_exclusive_ref_supports_definitions assert(schema.valid?(1)) refute(schema.valid?('1')) end + + def test_exclusive_ref_supports_definitions_with_id_and_json_pointer + schema = JSONSchemer.schema({ + '$schema' => 'http://json-schema.org/draft-07/schema#', + '$id' => 'https://example.com/schema', + '$ref' => '#/definitions/yah', + 'definitions' => { + 'yah' => { + '$id' => '#yah', + 'type' => 'integer' + } + } + }) + assert(schema.valid?(1)) + refute(schema.valid?('1')) + end end From 795a9b3689fde262d6f2b0a108abfff0dba8691b Mon Sep 17 00:00:00 2001 From: David Harsha Date: Fri, 13 Oct 2023 15:23:45 -0700 Subject: [PATCH 07/18] Simplify `result` method still ugly tho --- lib/json_schemer/output.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/json_schemer/output.rb b/lib/json_schemer/output.rb index 3d5c92c8..9ca0c13e 100644 --- a/lib/json_schemer/output.rb +++ b/lib/json_schemer/output.rb @@ -8,11 +8,7 @@ module Output private def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false) - if valid - Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations') - else - Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors') - end + Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, valid ? 'annotations' : 'errors') end def escaped_keyword From ccffac7130b5014705c4a128a41ec9bf9e937978 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Sat, 14 Oct 2023 11:16:16 -0700 Subject: [PATCH 08/18] Specify formats in meta schemas And use the same code paths as custom formats. This fixes an issue where older drafts have access to formats that aren't in the specification. --- lib/json_schemer.rb | 28 +-- lib/json_schemer/draft201909/meta.rb | 1 + lib/json_schemer/draft202012/meta.rb | 21 ++ .../draft202012/vocab/format_annotation.rb | 10 +- .../draft202012/vocab/format_assertion.rb | 8 +- lib/json_schemer/draft4/meta.rb | 4 + lib/json_schemer/draft6/meta.rb | 9 + lib/json_schemer/draft7/meta.rb | 3 + lib/json_schemer/format.rb | 203 ++++++++++-------- lib/json_schemer/openapi30/meta.rb | 5 + lib/json_schemer/openapi31/meta.rb | 8 + lib/json_schemer/schema.rb | 11 +- test/format_test.rb | 24 ++- 13 files changed, 198 insertions(+), 137 deletions(-) diff --git a/lib/json_schemer.rb b/lib/json_schemer.rb index f2c2817b..ecce26d7 100644 --- a/lib/json_schemer.rb +++ b/lib/json_schemer.rb @@ -145,6 +145,7 @@ def draft202012 @draft202012 ||= Schema.new( Draft202012::SCHEMA, :base_uri => Draft202012::BASE_URI, + :formats => Draft202012::FORMATS, :ref_resolver => Draft202012::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -154,6 +155,7 @@ def draft201909 @draft201909 ||= Schema.new( Draft201909::SCHEMA, :base_uri => Draft201909::BASE_URI, + :formats => Draft201909::FORMATS, :ref_resolver => Draft201909::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -164,6 +166,7 @@ def draft7 Draft7::SCHEMA, :vocabulary => { 'json-schemer://draft7' => true }, :base_uri => Draft7::BASE_URI, + :formats => Draft7::FORMATS, :regexp_resolver => 'ecma' ) end @@ -173,6 +176,7 @@ def draft6 Draft6::SCHEMA, :vocabulary => { 'json-schemer://draft6' => true }, :base_uri => Draft6::BASE_URI, + :formats => Draft6::FORMATS, :regexp_resolver => 'ecma' ) end @@ -182,6 +186,7 @@ def draft4 Draft4::SCHEMA, :vocabulary => { 'json-schemer://draft4' => true }, :base_uri => Draft4::BASE_URI, + :formats => Draft4::FORMATS, :regexp_resolver => 'ecma' ) end @@ -190,16 +195,9 @@ def openapi31 @openapi31 ||= Schema.new( OpenAPI31::SCHEMA, :base_uri => OpenAPI31::BASE_URI, + :formats => OpenAPI31::FORMATS, :ref_resolver => OpenAPI31::Meta::SCHEMAS.to_proc, - :regexp_resolver => 'ecma', - # https://spec.openapis.org/oas/latest.html#data-types - :formats => { - 'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 }, - 'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 }, - 'float' => proc { |instance, _value| instance.is_a?(Float) }, - 'double' => proc { |instance, _value| instance.is_a?(Float) }, - 'password' => proc { |_instance, _value| true } - } + :regexp_resolver => 'ecma' ) end @@ -211,17 +209,9 @@ def openapi30 'json-schemer://openapi30' => true }, :base_uri => OpenAPI30::BASE_URI, + :formats => OpenAPI30::FORMATS, :ref_resolver => OpenAPI30::Meta::SCHEMAS.to_proc, - :regexp_resolver => 'ecma', - :formats => { - 'int32' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 32 }, - 'int64' => proc { |instance, _value| instance.is_a?(Integer) && instance.bit_length <= 64 }, - 'float' => proc { |instance, _value| instance.is_a?(Float) }, - 'double' => proc { |instance, _value| instance.is_a?(Float) }, - 'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first }, - 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT }, - 'password' => proc { |_instance, _value| true } - } + :regexp_resolver => 'ecma' ) end diff --git a/lib/json_schemer/draft201909/meta.rb b/lib/json_schemer/draft201909/meta.rb index ac307a70..daeced90 100644 --- a/lib/json_schemer/draft201909/meta.rb +++ b/lib/json_schemer/draft201909/meta.rb @@ -2,6 +2,7 @@ module JSONSchemer module Draft201909 BASE_URI = URI('https://json-schema.org/draft/2019-09/schema') + FORMATS = Draft202012::FORMATS SCHEMA = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/schema', diff --git a/lib/json_schemer/draft202012/meta.rb b/lib/json_schemer/draft202012/meta.rb index e31d7625..14e47c81 100644 --- a/lib/json_schemer/draft202012/meta.rb +++ b/lib/json_schemer/draft202012/meta.rb @@ -2,6 +2,27 @@ module JSONSchemer module Draft202012 BASE_URI = URI('https://json-schema.org/draft/2020-12/schema') + FORMATS = { + 'date-time' => Format::DATE_TIME, + 'date' => Format::DATE, + 'time' => Format::TIME, + 'duration' => Format::DURATION, + 'email' => Format::EMAIL, + 'idn-email' => Format::IDN_EMAIL, + 'hostname' => Format::HOSTNAME, + 'idn-hostname' => Format::IDN_HOSTNAME, + 'ipv4' => Format::IPV4, + 'ipv6' => Format::IPV6, + 'uri' => Format::URI, + 'uri-reference' => Format::URI_REFERENCE, + 'iri' => Format::IRI, + 'iri-reference' => Format::IRI_REFERENCE, + 'uuid' => Format::UUID, + 'uri-template' => Format::URI_TEMPLATE, + 'json-pointer' => Format::JSON_POINTER, + 'relative-json-pointer' => Format::RELATIVE_JSON_POINTER, + 'regex' => Format::REGEX + } SCHEMA = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/schema', diff --git a/lib/json_schemer/draft202012/vocab/format_annotation.rb b/lib/json_schemer/draft202012/vocab/format_annotation.rb index 34c3b05b..9bc59cf7 100644 --- a/lib/json_schemer/draft202012/vocab/format_annotation.rb +++ b/lib/json_schemer/draft202012/vocab/format_annotation.rb @@ -4,20 +4,12 @@ module Draft202012 module Vocab module FormatAnnotation class Format < Keyword - extend JSONSchemer::Format - - DEFAULT_FORMAT = proc do |instance, value| - !instance.is_a?(String) || valid_spec_format?(instance, value) - rescue UnknownFormat - true - end - def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match format: #{value}" end def parse - root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) } + root.format && root.fetch_format(value, false) end def validate(instance, instance_location, keyword_location, _context) diff --git a/lib/json_schemer/draft202012/vocab/format_assertion.rb b/lib/json_schemer/draft202012/vocab/format_assertion.rb index 2ff6117c..05b5da16 100644 --- a/lib/json_schemer/draft202012/vocab/format_assertion.rb +++ b/lib/json_schemer/draft202012/vocab/format_assertion.rb @@ -4,18 +4,12 @@ module Draft202012 module Vocab module FormatAssertion class Format < Keyword - extend JSONSchemer::Format - - DEFAULT_FORMAT = proc do |instance, value| - !instance.is_a?(String) || valid_spec_format?(instance, value) - end - def error(formatted_instance_location:, **) "value at #{formatted_instance_location} does not match format: #{value}" end def parse - root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) } + root.format && root.fetch_format(value) { raise UnknownFormat, value } end def validate(instance, instance_location, keyword_location, _context) diff --git a/lib/json_schemer/draft4/meta.rb b/lib/json_schemer/draft4/meta.rb index ed3421c8..10fc977b 100644 --- a/lib/json_schemer/draft4/meta.rb +++ b/lib/json_schemer/draft4/meta.rb @@ -2,6 +2,10 @@ module JSONSchemer module Draft4 BASE_URI = URI('http://json-schema.org/draft-04/schema#') + FORMATS = Draft6::FORMATS.dup + FORMATS.delete('uri-reference') + FORMATS.delete('uri-template') + FORMATS.delete('json-pointer') SCHEMA = { 'id' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#', diff --git a/lib/json_schemer/draft6/meta.rb b/lib/json_schemer/draft6/meta.rb index 9ffa8842..5fb958e0 100644 --- a/lib/json_schemer/draft6/meta.rb +++ b/lib/json_schemer/draft6/meta.rb @@ -2,6 +2,15 @@ module JSONSchemer module Draft6 BASE_URI = URI('http://json-schema.org/draft-06/schema#') + FORMATS = Draft7::FORMATS.dup + FORMATS.delete('date') + FORMATS.delete('time') + FORMATS.delete('idn-email') + FORMATS.delete('idn-hostname') + FORMATS.delete('iri') + FORMATS.delete('iri-reference') + FORMATS.delete('relative-json-pointer') + FORMATS.delete('regex') SCHEMA = { '$schema' => 'http://json-schema.org/draft-06/schema#', '$id' => 'http://json-schema.org/draft-06/schema#', diff --git a/lib/json_schemer/draft7/meta.rb b/lib/json_schemer/draft7/meta.rb index 3405404a..34396da9 100644 --- a/lib/json_schemer/draft7/meta.rb +++ b/lib/json_schemer/draft7/meta.rb @@ -2,6 +2,9 @@ module JSONSchemer module Draft7 BASE_URI = URI('http://json-schema.org/draft-07/schema#') + FORMATS = Draft201909::FORMATS.dup + FORMATS.delete('duration') + FORMATS.delete('uuid') SCHEMA = { '$schema' => 'http://json-schema.org/draft-07/schema#', '$id' => 'http://json-schema.org/draft-07/schema#', diff --git a/lib/json_schemer/format.rb b/lib/json_schemer/format.rb index fcf239ff..c11e339e 100644 --- a/lib/json_schemer/format.rb +++ b/lib/json_schemer/format.rb @@ -1,11 +1,71 @@ # frozen_string_literal: true module JSONSchemer module Format - include Duration - include Email - include Hostname - include JSONPointer - include URITemplate + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3 + DATE_TIME = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?(instance) + end + DATE = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?("#{instance}T04:05:06.123456789+07:00") + end + TIME = proc do |instance, _format| + !instance.is_a?(String) || valid_date_time?("2001-02-03T#{instance}") + end + DURATION = proc do |instance, _format| + !instance.is_a?(String) || valid_duration?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.2 + EMAIL = proc do |instance, _format| + !instance.is_a?(String) || instance.ascii_only? && valid_email?(instance) + end + IDN_EMAIL = proc do |instance, _format| + !instance.is_a?(String) || valid_email?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.3 + HOSTNAME = proc do |instance, _format| + !instance.is_a?(String) || instance.ascii_only? && valid_hostname?(instance) + end + IDN_HOSTNAME = proc do |instance, _format| + !instance.is_a?(String) || valid_hostname?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.4 + IPV4 = proc do |instance, _format| + !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET) + end + IPV6 = proc do |instance, _format| + !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET6) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.5 + URI = proc do |instance, _format| + !instance.is_a?(String) || valid_uri?(instance) + end + URI_REFERENCE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_reference?(instance) + end + IRI = proc do |instance, _format| + !instance.is_a?(String) || valid_uri?(iri_escape(instance)) + end + IRI_REFERENCE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_reference?(iri_escape(instance)) + end + UUID = proc do |instance, _format| + !instance.is_a?(String) || valid_uuid?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.6 + URI_TEMPLATE = proc do |instance, _format| + !instance.is_a?(String) || valid_uri_template?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.7 + JSON_POINTER = proc do |instance, _format| + !instance.is_a?(String) || valid_json_pointer?(instance) + end + RELATIVE_JSON_POINTER = proc do |instance, _format| + !instance.is_a?(String) || valid_relative_json_pointer?(instance) + end + # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.8 + REGEX = proc do |instance, _format| + !instance.is_a?(String) || valid_regex?(instance) + end DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze HOUR_24_REGEX = /T24/.freeze @@ -20,6 +80,12 @@ module Format end.freeze class << self + include Duration + include Email + include Hostname + include JSONPointer + include URITemplate + def percent_encode(data, regexp) data = data.dup data.force_encoding(Encoding::ASCII_8BIT) @@ -52,101 +118,56 @@ def parse_content_media_type(data, content_media_type) raise UnknownContentMediaType, content_media_type end end - end - def valid_spec_format?(data, format) - case format - when 'date-time' - valid_date_time?(data) - when 'date' - valid_date_time?("#{data}T04:05:06.123456789+07:00") - when 'time' - valid_date_time?("2001-02-03T#{data}") - when 'email' - data.ascii_only? && valid_email?(data) - when 'idn-email' - valid_email?(data) - when 'hostname' - data.ascii_only? && valid_hostname?(data) - when 'idn-hostname' - valid_hostname?(data) - when 'ipv4' - valid_ip?(data, Socket::AF_INET) - when 'ipv6' - valid_ip?(data, Socket::AF_INET6) - when 'uri' - valid_uri?(data) - when 'uri-reference' - valid_uri_reference?(data) - when 'iri' - valid_uri?(iri_escape(data)) - when 'iri-reference' - valid_uri_reference?(iri_escape(data)) - when 'uri-template' - valid_uri_template?(data) - when 'json-pointer' - valid_json_pointer?(data) - when 'relative-json-pointer' - valid_relative_json_pointer?(data) - when 'regex' - valid_regex?(data) - when 'duration' - valid_duration?(data) - when 'uuid' - valid_uuid?(data) - else - raise UnknownFormat, format + def valid_date_time?(data) + return false if HOUR_24_REGEX.match?(data) + datetime = DateTime.rfc3339(data) + return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59' + DATE_TIME_OFFSET_REGEX.match?(data) + rescue ArgumentError + false end - end - - def valid_date_time?(data) - return false if HOUR_24_REGEX.match?(data) - datetime = DateTime.rfc3339(data) - return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59' - DATE_TIME_OFFSET_REGEX.match?(data) - rescue ArgumentError - false - end - def valid_ip?(data, family) - IPAddr.new(data, family) - IP_REGEX.match?(data) - rescue IPAddr::Error - false - end + def valid_ip?(data, family) + IPAddr.new(data, family) + IP_REGEX.match?(data) + rescue IPAddr::Error + false + end - def parse_uri_scheme(data) - scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data) - # URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should - raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque) - scheme - end + def parse_uri_scheme(data) + scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data) + # ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should + raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque) + scheme + end - def valid_uri?(data) - !!parse_uri_scheme(data) - rescue URI::InvalidURIError - false - end + def valid_uri?(data) + !!parse_uri_scheme(data) + rescue ::URI::InvalidURIError + false + end - def valid_uri_reference?(data) - parse_uri_scheme(data) - true - rescue URI::InvalidURIError - false - end + def valid_uri_reference?(data) + parse_uri_scheme(data) + true + rescue ::URI::InvalidURIError + false + end - def iri_escape(data) - Format.percent_encode(data, IRI_ESCAPE_REGEX) - end + def iri_escape(data) + Format.percent_encode(data, IRI_ESCAPE_REGEX) + end - def valid_regex?(data) - !!EcmaRegexp.ruby_equivalent(data) - rescue InvalidEcmaRegexp - false - end + def valid_regex?(data) + !!EcmaRegexp.ruby_equivalent(data) + rescue InvalidEcmaRegexp + false + end - def valid_uuid?(data) - UUID_REGEX.match?(data) || NIL_UUID == data + def valid_uuid?(data) + UUID_REGEX.match?(data) || NIL_UUID == data + end end end end diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index 4deee431..f89ed86c 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -2,6 +2,11 @@ module JSONSchemer module OpenAPI30 BASE_URI = URI('json-schemer://openapi30/schema') + # https://spec.openapis.org/oas/v3.0.3#data-types + FORMATS = OpenAPI31::FORMATS.merge( + 'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first }, + 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT } + ) SCHEMA = { 'id' => 'json-schemer://openapi30/schema', '$schema' => 'http://json-schema.org/draft-04/schema#', diff --git a/lib/json_schemer/openapi31/meta.rb b/lib/json_schemer/openapi31/meta.rb index 85e7c11e..e9bb7ac9 100644 --- a/lib/json_schemer/openapi31/meta.rb +++ b/lib/json_schemer/openapi31/meta.rb @@ -2,6 +2,14 @@ module JSONSchemer module OpenAPI31 BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base') + # https://spec.openapis.org/oas/v3.1.0#data-types + FORMATS = { + 'int32' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 32 }, + 'int64' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 64 }, + 'float' => proc { |instance, _format| instance.is_a?(Float) }, + 'double' => proc { |instance, _format| instance.is_a?(Float) }, + 'password' => proc { |_instance, _format| true } + } SCHEMA = { '$id' => 'https://spec.openapis.org/oas/3.1/dialect/base', '$schema' => 'https://json-schema.org/draft/2020-12/schema', diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index bdce6b8c..3c6ed8d4 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -10,7 +10,6 @@ def original_instance(instance_location) end include Output - include Format::JSONPointer DEFAULT_SCHEMA = Draft202012::BASE_URI.to_s.freeze SCHEMA_KEYWORD_CLASS = Draft202012::Vocab::Core::Schema @@ -160,7 +159,7 @@ def validate_instance(instance, instance_location, keyword_location, context) def resolve_ref(uri) pointer = '' - if valid_json_pointer?(uri.fragment) + if Format.valid_json_pointer?(uri.fragment) pointer = URI.decode_www_form_component(uri.fragment) uri.fragment = nil end @@ -288,6 +287,14 @@ def schema_pointer end end + def fetch_format(format, *args, &block) + if meta_schema == self + formats.fetch(format, *args, &block) + else + formats.fetch(format) { meta_schema.fetch_format(format, *args, &block) } + end + end + def id_keyword @id_keyword ||= (keywords.key?('$id') ? '$id' : 'id') end diff --git a/test/format_test.rb b/test/format_test.rb index b0599279..a1deec45 100644 --- a/test/format_test.rb +++ b/test/format_test.rb @@ -21,22 +21,28 @@ def test_it_ignores_unknown_format schemer = JSONSchemer.schema({ 'type' => 'string', 'format' => 'unknown' }) assert(schemer.valid?('1')) refute(schemer.valid?(1)) + schemer = JSONSchemer.schema({ 'maximum' => 1, 'format' => 'unknown' }) + assert(schemer.valid?(1)) + refute(schemer.valid?(2)) end def test_format_assertion_raises_unknown_format - meta = { + annotation = { + '$vocabulary' => { + 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true + } + } + assertion = { '$vocabulary' => { 'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true } } - schemer = JSONSchemer.schema( - { - '$schema' => 'http://example.com', - 'format' => 'unknown' - }, - :ref_resolver => proc { meta } - ) - assert_raises(JSONSchemer::UnknownFormat) { schemer.validate('anything') } + schema = { + '$schema' => 'http://example.com', + 'format' => 'unknown' + } + assert(JSONSchemer.schema(schema, :ref_resolver => proc { annotation }).valid?('x')) + assert_raises(JSONSchemer::UnknownFormat) { JSONSchemer.schema(schema, :ref_resolver => proc { assertion }) } end def test_it_validates_spaces_in_uri_format From 48ee19155b4586e5ceaa617fe6c52d2e01f0dff2 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Sun, 15 Oct 2023 14:09:12 -0700 Subject: [PATCH 09/18] Support custom error messages This introduces two ways of defining custom error messages: the `x-error` keyword and I18n translations. `x-error` is a json_schemer-specific schema keyword that allows overriding error messsages for a schema. It can either be a string, which overrides errors for all keywords (and for the schema itself), or a hash of keyword-specific messages. Interpolation of some values is supported using `gsub` because `sprintf` logs annoying warnings if all arguments aren't present in the string. I18n translations are enabled when the `i18n` gem is loaded and a `json_schemer` translation key exists. Translation keys are based on schema $id, keyword location, meta-schema $id, and keyword. The lookup is ordered from most specific to least specific so that overrides can be applied as necessary. Notes: - Both `x-error` and I18n use special catch-all (`*`) and schema (`^`) "keywords" to support fallbacks and schema-level errors, respectively. - `x-error` uses the `x-` prefix to match the future spec for unknown keywords: https://github.com/orgs/json-schema-org/discussions/329 - The I18n check to enable translations is cached forever because I18n is slow and I didn't want it to hurt performance when it's not used. - I18n uses the ASCII unit separator (\x1F) because the `.` default doesn't work well with absolute URIs ($id and $schema). - I moved `CLASSIC_ERROR_TYPES` out of `JSONSchemer::Result` because that's actually where it's being defined. The `Struct.new` block scope doesn't hold constants like a class. Closes: https://github.com/davishmcclurg/json_schemer/issues/143 --- Gemfile.lock | 7 + README.md | 110 ++++++++++ json_schemer.gemspec | 2 + lib/json_schemer/draft202012/vocab.rb | 4 +- lib/json_schemer/draft202012/vocab/core.rb | 6 + lib/json_schemer/keyword.rb | 4 + lib/json_schemer/output.rb | 5 + lib/json_schemer/result.rb | 66 +++++- lib/json_schemer/schema.rb | 4 + test/errors_test.rb | 243 +++++++++++++++++++++ test/exe_test.rb | 2 + 11 files changed, 444 insertions(+), 9 deletions(-) create mode 100644 test/errors_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 8e3e063e..5f08ad3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,8 +9,13 @@ PATH GEM remote: https://rubygems.org/ specs: + concurrent-ruby (1.2.2) docile (1.4.0) hana (1.3.7) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + i18n-debug (1.2.0) + i18n (< 2) minitest (5.15.0) rake (13.0.6) regexp_parser (2.8.1) @@ -33,6 +38,8 @@ PLATFORMS DEPENDENCIES bundler (~> 2.0) + i18n + i18n-debug json_schemer! minitest (~> 5.0) rake (~> 13.0) diff --git a/README.md b/README.md index b119fa6a..e15946bd 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,116 @@ JSONSchemer.schema( ) ``` +## Custom Error Messages + +Error messages can be customized using the `x-error` keyword and/or [I18n](https://github.com/ruby-i18n/i18n) translations. `x-error` takes precedence if both are defined. + +### `x-error` Keyword + +```ruby +# override all errors for a schema +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'x-error' => 'custom error for schema and all keywords' +}) + +schemer.validate(1).map { _1.fetch('error') } +# => ["custom error for schema and all keywords"] + +schemer.validate(1, :output_format => 'basic').fetch('error') +# => "custom error for schema and all keywords" + +# keyword-specific errors +schemer = JSONSchemer.schema({ + 'type' => 'string', + 'minLength' => 10, + 'x-error' => { + 'type' => 'custom error for `type` keyword', + # special `^` keyword for schema-level error + '^' => 'custom error for schema', + # same behavior as when `x-error` is a string + '*' => 'fallback error for schema and all keywords' + } +}) + +schemer.validate(1).map { _1.fetch('error') } +# => ["custom error for `type` keyword"] + +schemer.validate('1').map { _1.fetch('error') } +# => ["custom error for schema and all keywords"] + +schemer.validate(1, :output_format => 'basic').fetch('error') +# => "custom error for schema" +``` + +### I18n + +When the [I18n gem](https://github.com/ruby-i18n/i18n) is loaded, custom error messages are looked up under the `json_schemer` key. It may be necessary to restart your application after adding the root key because the existence check is cached for performance reasons. + +Translation keys are looked up in this order: + +1. `$LOCALE.json_schemer.errors.$ABSOLUTE_KEYWORD_LOCATION` +2. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD_LOCATION` +3. `$LOCALE.json_schemer.errors.$KEYWORD_LOCATION` +4. `$LOCALE.json_schemer.errors.$SCHEMA_ID.$KEYWORD` +5. `$LOCALE.json_schemer.errors.$SCHEMA_ID.*` +6. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.$KEYWORD` +7. `$LOCALE.json_schemer.errors.$META_SCHEMA_ID.*` +8. `$LOCALE.json_schemer.errors.$KEYWORD` +9. `$LOCALE.json_schemer.errors.*` + +For example, this validation: + +```ruby +I18n.locale = :en # $LOCALE=en + +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', # $SCHEMA_ID=https://example.com/schema + '$schema' => 'http://json-schema.org/draft-07/schema#', # $META_SCHEMA_ID=http://json-schema.org/draft-07/schema# + 'properties' => { + 'abc' => { + 'type' => 'integer' # $KEYWORD=type + } # $KEYWORD_LOCATION=#/properties/abc/type + } # $ABSOLUTE_KEYWORD_LOCATION=https://example.com/schema#/properties/abc/type +}) + +schemer.validate({ 'abc' => 'not-an-integer' }).to_a +``` + +looks up custom error messages using these keys (in order): + +1. `en.json_schemer.errors.'https://example.com/schema#/properties/abc/type'` +2. `en.json_schemer.errors.'https://example.com/schema'.'#/properties/abc/type'` +3. `en.json_schemer.errors.'#/properties/abc/type'` +4. `en.json_schemer.errors.'https://example.com/schema'.type` +5. `en.json_schemer.errors.'https://example.com/schema'.*` +6. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.type` +7. `en.json_schemer.errors.'http://json-schema.org/draft-07/schema#'.*` +8. `en.json_schemer.errors.type` +9. `en.json_schemer.errors.*` + +Example translations file: + +```yaml +en: + json_schemer: + errors: + 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location + 'https://example.com/schema': + '#/properties/abc/type': custom error for keyword location, nested under schema $id + 'type': custom error for `type` keyword, nested under schema $id + '^': custom error for schema, nested under schema $id + '*': fallback error for schema and all keywords, nested under schema $id + '#/properties/abc/type': custom error for keyword location + 'http://json-schema.org/draft-07/schema#': + 'type': custom error for `type` keyword, nested under meta-schema $id ($schema) + '^': custom error for schema, nested under meta-schema $id + '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema) + 'type': custom error for `type` keyword + '^': custom error for schema + '*': fallback error for schema and all keywords +``` + ## OpenAPI ```ruby diff --git a/json_schemer.gemspec b/json_schemer.gemspec index be40b9d0..122a2dc6 100644 --- a/json_schemer.gemspec +++ b/json_schemer.gemspec @@ -26,6 +26,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.0" spec.add_development_dependency "simplecov", "~> 0.22" + spec.add_development_dependency "i18n" + spec.add_development_dependency "i18n-debug" spec.add_runtime_dependency "hana", "~> 1.3" spec.add_runtime_dependency "regexp_parser", "~> 2.0" diff --git a/lib/json_schemer/draft202012/vocab.rb b/lib/json_schemer/draft202012/vocab.rb index 7eaafc51..a77b70b0 100644 --- a/lib/json_schemer/draft202012/vocab.rb +++ b/lib/json_schemer/draft202012/vocab.rb @@ -16,7 +16,9 @@ module Vocab '$defs' => Core::Defs, 'definitions' => Core::Defs, # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-8.3 - '$comment' => Core::Comment + '$comment' => Core::Comment, + # https://github.com/orgs/json-schema-org/discussions/329 + 'x-error' => Core::XError } # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01#section-10 APPLICATOR = { diff --git a/lib/json_schemer/draft202012/vocab/core.rb b/lib/json_schemer/draft202012/vocab/core.rb index 8d288d06..66fea2c1 100644 --- a/lib/json_schemer/draft202012/vocab/core.rb +++ b/lib/json_schemer/draft202012/vocab/core.rb @@ -119,6 +119,12 @@ def parse class Comment < Keyword; end + class XError < Keyword + def message(error_key) + value.is_a?(Hash) ? (value[error_key] || value[CATCHALL]) : value + end + end + class UnknownKeyword < Keyword def parse if value.is_a?(Hash) diff --git a/lib/json_schemer/keyword.rb b/lib/json_schemer/keyword.rb index 5b8a5cc1..091d7d8e 100644 --- a/lib/json_schemer/keyword.rb +++ b/lib/json_schemer/keyword.rb @@ -26,6 +26,10 @@ def schema_pointer @schema_pointer ||= "#{parent.schema_pointer}/#{escaped_keyword}" end + def error_key + keyword + end + private def parse diff --git a/lib/json_schemer/output.rb b/lib/json_schemer/output.rb index 9ca0c13e..74c55ec6 100644 --- a/lib/json_schemer/output.rb +++ b/lib/json_schemer/output.rb @@ -5,6 +5,11 @@ module Output attr_reader :keyword, :schema + def x_error + return @x_error if defined?(@x_error) + @x_error = schema.parsed['x-error']&.message(error_key) + end + private def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false) diff --git a/lib/json_schemer/result.rb b/lib/json_schemer/result.rb index 3612c892..e0249038 100644 --- a/lib/json_schemer/result.rb +++ b/lib/json_schemer/result.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true module JSONSchemer - Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do - CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| - hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) - end + CATCHALL = '*' + I18N_SEPARATOR = "\x1F" # unit separator + I18N_SCOPE = 'json_schemer' + I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors" + X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/ + CLASSIC_ERROR_TYPES = Hash.new do |hash, klass| + hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase) + end + Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do def output(output_format) case output_format when 'classic' @@ -24,10 +29,55 @@ def output(output_format) def error return @error if defined?(@error) - resolved_instance_location = Location.resolve(instance_location) - @error = source.error( - :formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`", - :details => details + if source.x_error + # not using sprintf because it warns: "too many arguments for format string" + @error = source.x_error.gsub( + X_ERROR_REGEX, + '%{instance}' => instance, + '%{instanceLocation}' => Location.resolve(instance_location), + '%{keywordLocation}' => Location.resolve(keyword_location), + '%{absoluteKeywordLocation}' => source.absolute_keyword_location + ) + else + resolved_instance_location = Location.resolve(instance_location) + formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`" + @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details) + @error = i18n(@error) if i18n? + end + @error + end + + def i18n? + return @@i18n if defined?(@@i18n) + @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE) + end + + def i18n(default) + base_uri_str = source.schema.base_uri.to_s + meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s + resolved_keyword_location = Location.resolve(keyword_location) + error_key = source.error_key + keys = [ + "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}", + "##{resolved_keyword_location}", + "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + error_key, + CATCHALL + ] + keys.map!(&:to_sym) + keys << @error + @error = I18n.t( + source.absolute_keyword_location, + :scope => I18N_ERRORS_SCOPE, + :default => keys, + :separator => I18N_SEPARATOR, + :instance => instance, + :instanceLocation => Location.resolve(instance_location), + :keywordLocation => resolved_keyword_location, + :absoluteKeywordLocation => source.absolute_keyword_location ) end diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index 3c6ed8d4..06cb2b31 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -287,6 +287,10 @@ def schema_pointer end end + def error_key + '^' + end + def fetch_format(format, *args, &block) if meta_schema == self formats.fetch(format, *args, &block) diff --git a/test/errors_test.rb b/test/errors_test.rb new file mode 100644 index 00000000..ba6a2106 --- /dev/null +++ b/test/errors_test.rb @@ -0,0 +1,243 @@ +require 'test_helper' + +class ErrorsTest < Minitest::Test + def test_x_error + schema = { + 'oneOf' => [ + { + 'x-error' => 'properties a and b were provided, however only one or the other may be specified', + 'required' => ['a'], + 'not' => { 'required' => ['b'] } + }, + { + 'x-error' => { + 'not' => '%{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}' + }, + 'required' => ['b'], + 'not' => { 'required' => ['a'] } + } + ], + 'x-error' => { + '*' => 'schema error', + 'oneOf' => 'oneOf error' + } + } + data = { + 'a' => 'foo', + 'b' => 'bar' + } + assert_equal( + [ + 'properties a and b were provided, however only one or the other may be specified', + '{"a"=>"foo", "b"=>"bar"} `` /oneOf/1/not json-schemer://schema#/oneOf/1/not' + ].sort, + JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('error') }.sort + ) + + assert_equal('schema error', JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('error')) + assert_equal('oneOf error', JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('error')) + end + + def test_x_error_override + schema = { + 'required' => ['a'], + 'minProperties' => 2 + } + assert_equal( + ['object at root is missing required properties: a', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => 'schema error') + assert_equal( + ['schema error', 'schema error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { 'required' => 'required error' }) + assert_equal( + ['required error', 'object size at root is less than: 2'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { 'required' => 'required error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['required error', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + + schema.merge!('x-error' => { '*' => 'catchall', 'minProperties' => 'minProperties error' }) + assert_equal( + ['catchall', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'catchall', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', 'minProperties' => 'minProperties error' }) + assert_equal( + ['object at root is missing required properties: a', 'minProperties error'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + + schema.merge!('x-error' => { '^' => 'schema error', '*' => 'catchall' }) + assert_equal( + ['catchall', 'catchall'].sort, + JSONSchemer.schema(schema).validate({}).map { |error| error.fetch('error') }.sort + ) + assert_equal( + 'schema error', + JSONSchemer.schema(schema).validate({}, :output_format => 'basic').fetch('error') + ) + end + + def test_x_error_precedence + schema = { + '$id' => 'https://example.com/schema', + 'required' => ['a'] + } + x_error_schema = schema.merge( + 'x-error' => { + 'required' => 'x error' + } + ) + + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('object at root is missing required properties: a', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + + i18n({ 'https://example.com/schema#/required' => 'i18n error' }) do + assert_equal('x error', JSONSchemer.schema(x_error_schema).validate({}).first.fetch('error')) + assert_equal('i18n error', JSONSchemer.schema(schema).validate({}).first.fetch('error')) + end + end + + def test_i18n_error + schema = { + '$id' => 'https://example.com/schema', + '$schema' => 'https://json-schema.org/draft/2019-09/schema', + 'properties' => { + 'yah' => { + 'type' => 'string' + } + } + } + schemer = JSONSchemer.schema(schema) + data = { 'yah' => 1 } + + errors = { + 'https://example.com/schema#' => 'A', + 'https://example.com/schema#/properties/yah/type' => '1', + 'https://example.com/schema' => { + '#' => 'B', + '#/properties/yah/type' => '2', + '^' => 'D', + 'type' => '4', + '*' => 'E/5' + }, + '#/properties/yah/type' => '3', + '#' => 'C', + 'https://json-schema.org/draft/2019-09/schema' => { + '^' => 'F', + 'type' => '6', + '*' => 'G/7' + }, + '^' => 'H', + 'type' => '8', + '*' => 'I/9', + + 'https://example.com/differentschema#/properties/yah/type' => '?', + 'https://example.com/differentschema' => { + '#/properties/yah/type' => '?', + 'type' => '?', + '*' => '?' + }, + '?' => '?' + } + assert_equal('A', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + assert_equal('1', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('https://example.com/schema#') + assert_equal('B', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('https://example.com/schema#/properties/yah/type') + assert_equal('2', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#') + assert_equal('C', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('#/properties/yah/type') + assert_equal('3', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('#') + assert_equal('D', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('#/properties/yah/type') + assert_equal('4', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('^') + assert_equal('E/5', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://example.com/schema').delete('type') + assert_equal('E/5', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://example.com/schema').delete('*') + assert_equal('F', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + assert_equal('6', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('^') + assert_equal('G/7', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('type') + assert_equal('G/7', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('*') + assert_equal('H', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + assert_equal('8', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('^') + assert_equal('I/9', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + + errors.delete('type') + assert_equal('I/9', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + errors.delete('*') + assert_equal('value at root does not match schema', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + assert_equal('value at `/yah` is not a string', i18n(errors) { schemer.validate(data).first.fetch('error') }) + end + +private + + def i18n(errors) + require 'yaml' + require 'i18n' + # require 'i18n/debug' + + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # @on_lookup ||= I18n::Debug.on_lookup + # I18n::Debug.on_lookup(&@on_lookup) + + Tempfile.create(['translations', '.yml']) do |file| + file.write(YAML.dump({ 'en' => { 'json_schemer' => { 'errors' => errors } } })) + file.flush + + I18n.load_path += [file.path] + + yield + ensure + I18n.load_path -= [file.path] + end + ensure + JSONSchemer.remove_class_variable(:@@i18n) if JSONSchemer.class_variable_defined?(:@@i18n) + # I18n::Debug.on_lookup {} + end +end diff --git a/test/exe_test.rb b/test/exe_test.rb index 8f37a1c1..9b73a386 100644 --- a/test/exe_test.rb +++ b/test/exe_test.rb @@ -175,7 +175,9 @@ def test_stdin def exe(*args, **kwargs) Open3.capture3('bundle', 'exec', 'json_schemer', *args, **kwargs).tap do |_stdout, stderr, _status| + # :nocov: stderr.gsub!(RUBY_2_5_WARNING_REGEX, '') if RUBY_ENGINE == 'ruby' && RUBY_VERSION.match?(/\A2\.5\.\d+\z/) + # :nocov: end end From 2c3112d75b7f9093266d68133983c8f4403b6aea Mon Sep 17 00:00:00 2001 From: David Harsha Date: Mon, 23 Oct 2023 19:45:43 -0700 Subject: [PATCH 10/18] Add `x-error` and `i18n` keys to relevant errors These keys make it possible to determine how error messages were generated. https://github.com/davishmcclurg/json_schemer/pull/149#issuecomment-1771993047 --- README.md | 110 +++++++++++++++++++++++++++---------- lib/json_schemer/result.rb | 49 ++++++++++------- test/errors_test.rb | 51 +++++++++++++---- 3 files changed, 150 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index e15946bd..6a20f3a6 100644 --- a/README.md +++ b/README.md @@ -238,11 +238,24 @@ schemer = JSONSchemer.schema({ 'x-error' => 'custom error for schema and all keywords' }) -schemer.validate(1).map { _1.fetch('error') } -# => ["custom error for schema and all keywords"] - -schemer.validate(1, :output_format => 'basic').fetch('error') -# => "custom error for schema and all keywords" +schemer.validate(1).first +# => {"data"=>1, +# "data_pointer"=>"", +# "schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "schema_pointer"=>"", +# "root_schema"=>{"type"=>"string", "x-error"=>"custom error for schema and all keywords"}, +# "type"=>"string", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true} + +schemer.validate(1, :output_format => 'basic') +# => {"valid"=>false, +# "keywordLocation"=>"", +# "absoluteKeywordLocation"=>"json-schemer://schema#", +# "instanceLocation"=>"", +# "error"=>"custom error for schema and all keywords", +# "x-error"=>true, +# "errors"=>#} # keyword-specific errors schemer = JSONSchemer.schema({ @@ -265,6 +278,28 @@ schemer.validate('1').map { _1.fetch('error') } schemer.validate(1, :output_format => 'basic').fetch('error') # => "custom error for schema" + +# variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) +schemer = JSONSchemer.schema({ + '$id' => 'https://example.com/schema', + 'properties' => { + 'abc' => { + 'type' => 'string', + 'x-error' => <<~ERROR + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} + ERROR + } + } +}) + +puts schemer.validate({ 'abc' => 1 }).first.fetch('error') +# instance: 1 +# instance location: /abc +# keyword location: /properties/abc/type +# absolute keyword location: https://example.com/schema#/properties/abc/type ``` ### I18n @@ -283,9 +318,38 @@ Translation keys are looked up in this order: 8. `$LOCALE.json_schemer.errors.$KEYWORD` 9. `$LOCALE.json_schemer.errors.*` -For example, this validation: +Example translations file: + +```yaml +en: + json_schemer: + errors: + 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location + 'https://example.com/schema': + '#/properties/abc/type': custom error for keyword location, nested under schema $id + 'type': custom error for `type` keyword, nested under schema $id + '^': custom error for schema, nested under schema $id + '*': fallback error for schema and all keywords, nested under schema $id + '#/properties/abc/type': custom error for keyword location + 'http://json-schema.org/draft-07/schema#': + 'type': custom error for `type` keyword, nested under meta-schema $id ($schema) + '^': custom error for schema, nested under meta-schema $id + '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema) + 'type': custom error for `type` keyword + '^': custom error for schema + # variable interpolation (instance/instanceLocation/keywordLocation/absoluteKeywordLocation) + '*': | + fallback error for schema and all keywords + instance: %{instance} + instance location: %{instanceLocation} + keyword location: %{keywordLocation} + absolute keyword location: %{absoluteKeywordLocation} +``` + +And output: ```ruby +require 'i18n' I18n.locale = :en # $LOCALE=en schemer = JSONSchemer.schema({ @@ -298,10 +362,18 @@ schemer = JSONSchemer.schema({ } # $ABSOLUTE_KEYWORD_LOCATION=https://example.com/schema#/properties/abc/type }) -schemer.validate({ 'abc' => 'not-an-integer' }).to_a +schemer.validate({ 'abc' => 'not-an-integer' }).first +# => {"data"=>"not-an-integer", +# "data_pointer"=>"/abc", +# "schema"=>{"type"=>"integer"}, +# "schema_pointer"=>"/properties/abc", +# "root_schema"=>{"$id"=>"https://example.com/schema", "$schema"=>"http://json-schema.org/draft-07/schema#", "properties"=>{"abc"=>{"type"=>"integer"}}}, +# "type"=>"integer", +# "error"=>"custom error for absolute keyword location", +# "i18n"=>true ``` -looks up custom error messages using these keys (in order): +In the example above, custom error messsages are looked up using the following keys (in order until one is found): 1. `en.json_schemer.errors.'https://example.com/schema#/properties/abc/type'` 2. `en.json_schemer.errors.'https://example.com/schema'.'#/properties/abc/type'` @@ -313,28 +385,6 @@ looks up custom error messages using these keys (in order): 8. `en.json_schemer.errors.type` 9. `en.json_schemer.errors.*` -Example translations file: - -```yaml -en: - json_schemer: - errors: - 'https://example.com/schema#/properties/abc/type': custom error for absolute keyword location - 'https://example.com/schema': - '#/properties/abc/type': custom error for keyword location, nested under schema $id - 'type': custom error for `type` keyword, nested under schema $id - '^': custom error for schema, nested under schema $id - '*': fallback error for schema and all keywords, nested under schema $id - '#/properties/abc/type': custom error for keyword location - 'http://json-schema.org/draft-07/schema#': - 'type': custom error for `type` keyword, nested under meta-schema $id ($schema) - '^': custom error for schema, nested under meta-schema $id - '*': fallback error for schema and all keywords, nested under meta-schema $id ($schema) - 'type': custom error for `type` keyword - '^': custom error for schema - '*': fallback error for schema and all keywords -``` - ## OpenAPI ```ruby diff --git a/lib/json_schemer/result.rb b/lib/json_schemer/result.rb index e0249038..52b05d95 100644 --- a/lib/json_schemer/result.rb +++ b/lib/json_schemer/result.rb @@ -38,11 +38,18 @@ def error '%{keywordLocation}' => Location.resolve(keyword_location), '%{absoluteKeywordLocation}' => source.absolute_keyword_location ) + @x_error = true else resolved_instance_location = Location.resolve(instance_location) formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`" @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details) - @error = i18n(@error) if i18n? + if i18n? + begin + @error = i18n! + @i18n = true + rescue I18n::MissingTranslationData + end + end end @error end @@ -52,28 +59,25 @@ def i18n? @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE) end - def i18n(default) + def i18n! base_uri_str = source.schema.base_uri.to_s meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s resolved_keyword_location = Location.resolve(keyword_location) error_key = source.error_key - keys = [ - "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}", - "##{resolved_keyword_location}", - "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}", - "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", - "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}", - "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", - error_key, - CATCHALL - ] - keys.map!(&:to_sym) - keys << @error - @error = I18n.t( + I18n.translate!( source.absolute_keyword_location, - :scope => I18N_ERRORS_SCOPE, - :default => keys, + :default => [ + "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}", + "##{resolved_keyword_location}", + "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}", + "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}", + error_key, + CATCHALL + ].map!(&:to_sym), :separator => I18N_SEPARATOR, + :scope => I18N_ERRORS_SCOPE, :instance => instance, :instanceLocation => Location.resolve(instance_location), :keywordLocation => resolved_keyword_location, @@ -88,8 +92,13 @@ def to_output_unit 'absoluteKeywordLocation' => source.absolute_keyword_location, 'instanceLocation' => Location.resolve(instance_location) } - out['error'] = error unless valid - out['annotation'] = annotation if valid && annotation + if valid + out['annotation'] = annotation if annotation + else + out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n + end out end @@ -104,6 +113,8 @@ def to_classic 'type' => type || CLASSIC_ERROR_TYPES[source.class] } out['error'] = error + out['x-error'] = true if @x_error + out['i18n'] = true if @i18n out['details'] = details if details out end diff --git a/test/errors_test.rb b/test/errors_test.rb index ba6a2106..6a57bd9c 100644 --- a/test/errors_test.rb +++ b/test/errors_test.rb @@ -36,6 +36,14 @@ def test_x_error assert_equal('schema error', JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('error')) assert_equal('oneOf error', JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('error')) + + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'basic').fetch('x-error')) + assert_equal(true, JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').fetch('x-error')) + assert_equal([true, true], JSONSchemer.schema(schema).validate(data).map { |error| error.fetch('x-error') }) + + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'basic').key?('i18n')) + refute(JSONSchemer.schema(schema).validate(data, :output_format => 'detailed').key?('i18n')) + assert_equal([false, false], JSONSchemer.schema(schema).validate(data).map { |error| error.key?('i18n') }) end def test_x_error_override @@ -153,7 +161,7 @@ def test_i18n_error }, '^' => 'H', 'type' => '8', - '*' => 'I/9', + '*' => 'I/9: %{instance} `%{instanceLocation}` %{keywordLocation} %{absoluteKeywordLocation}', 'https://example.com/differentschema#/properties/yah/type' => '?', 'https://example.com/differentschema' => { @@ -163,8 +171,11 @@ def test_i18n_error }, '?' => '?' } - assert_equal('A', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) - assert_equal('1', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + i18n(errors) do + assert_equal('A', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('1', schemer.validate(data).first.fetch('error')) + end errors.delete('https://example.com/schema#') assert_equal('B', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) @@ -191,8 +202,10 @@ def test_i18n_error assert_equal('E/5', i18n(errors) { schemer.validate(data).first.fetch('error') }) errors.fetch('https://example.com/schema').delete('*') - assert_equal('F', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) - assert_equal('6', i18n(errors) { schemer.validate(data).first.fetch('error') }) + i18n(errors) do + assert_equal('F', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('6', schemer.validate(data).first.fetch('error')) + end errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('^') assert_equal('G/7', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) @@ -201,18 +214,34 @@ def test_i18n_error assert_equal('G/7', i18n(errors) { schemer.validate(data).first.fetch('error') }) errors.fetch('https://json-schema.org/draft/2019-09/schema').delete('*') - assert_equal('H', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) - assert_equal('8', i18n(errors) { schemer.validate(data).first.fetch('error') }) + i18n(errors) do + assert_equal('H', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('8', schemer.validate(data).first.fetch('error')) + end errors.delete('^') - assert_equal('I/9', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) + assert_equal('I/9: {"yah"=>1} `` https://example.com/schema#', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) errors.delete('type') - assert_equal('I/9', i18n(errors) { schemer.validate(data).first.fetch('error') }) + assert_equal('I/9: 1 `/yah` /properties/yah/type https://example.com/schema#/properties/yah/type', i18n(errors) { schemer.validate(data).first.fetch('error') }) + + i18n(errors) do + assert_equal(true, schemer.validate(data).first.fetch('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + assert_equal(true, schemer.validate(data, :output_format => 'basic').fetch('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end errors.delete('*') - assert_equal('value at root does not match schema', i18n(errors) { schemer.validate(data, :output_format => 'basic').fetch('error') }) - assert_equal('value at `/yah` is not a string', i18n(errors) { schemer.validate(data).first.fetch('error') }) + i18n(errors) do + assert_equal('value at root does not match schema', schemer.validate(data, :output_format => 'basic').fetch('error')) + assert_equal('value at `/yah` is not a string', schemer.validate(data).first.fetch('error')) + + refute(schemer.validate(data).first.key?('i18n')) + refute(schemer.validate(data).first.key?('x-error')) + refute(schemer.validate(data, :output_format => 'basic').key?('i18n')) + refute(schemer.validate(data, :output_format => 'basic').key?('x-error')) + end end private From 69fe7a815ecf0cfb1c40ac402bf46a789c05e972 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Tue, 24 Oct 2023 12:02:54 -0700 Subject: [PATCH 11/18] Add `date` format to OpenAPI 3.0 This shouldn't've been removed in: ccffac7130b5014705c4a128a41ec9bf9e937978 I was under the impression that it would be provided by draft4 since that's openapi30's meta schema, but it's not in that spec. OpenAPI 3.0 formats listed here: https://spec.openapis.org/oas/v3.0.3#data-types @ahx noticed this here: https://github.com/davishmcclurg/json_schemer/pull/149#issuecomment-1776877751 --- lib/json_schemer/openapi30/meta.rb | 3 ++- test/open_api_test.rb | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index f89ed86c..c5bcf478 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -5,7 +5,8 @@ module OpenAPI30 # https://spec.openapis.org/oas/v3.0.3#data-types FORMATS = OpenAPI31::FORMATS.merge( 'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first }, - 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT } + 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT }, + 'date' => Format::DATE ) SCHEMA = { 'id' => 'json-schemer://openapi30/schema', diff --git a/test/open_api_test.rb b/test/open_api_test.rb index 72ff2fbb..5b472fa5 100644 --- a/test/open_api_test.rb +++ b/test/open_api_test.rb @@ -735,7 +735,9 @@ def test_openapi30_formats 'd' => { 'format' => 'double' }, 'e' => { 'format' => 'password' }, 'f' => { 'format' => 'byte' }, - 'g' => { 'format' => 'binary' } + 'g' => { 'format' => 'binary' }, + 'h' => { 'format' => 'date' }, + 'i' => { 'format' => 'date-time' } } } @@ -756,6 +758,10 @@ def test_openapi30_formats assert(schemer.valid?({ 'f' => 'IQ==' })) refute(schemer.valid?({ 'g' => '!' })) assert(schemer.valid?({ 'g' => '!'.b })) + refute(schemer.valid?({ 'h' => '2001-02-03T04:05:06.123456789+07:00' })) + assert(schemer.valid?({ 'h' => '2001-02-03' })) + refute(schemer.valid?({ 'i' => '2001-02-03' })) + assert(schemer.valid?({ 'i' => '2001-02-03T04:05:06.123456789+07:00' })) end def test_unsupported_openapi_version From febaf26d52a7b752ef6a456b8bb5cc2c95fa6b06 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Sat, 14 Oct 2023 12:53:35 -0700 Subject: [PATCH 12/18] Support custom content encodings and media types This exposes hooks for the `contentEncoding` and `contentMediaType` keywords, similar to the existing custom formats behavior. The provided callables must return a tuple comprised of a validation boolean and annotation of any type. The validation boolean is ignored in draft 2019-09 and 2020-12, because the [specification][0] says: > They do not function as validation assertions; a malformed string-encoded document MUST NOT cause the containing instance to be considered invalid. Drafts 7 and earlier will return a validation error based on the validation boolean. From the [specification][1]: > Implementations MAY support the "contentMediaType" and "contentEncoding" keywords as validation assertions. All drafts forward the returned annotation as an annotation in the overall result. I don't love the API here, since it requires returning an array even when it's ignored in the latest drafts, but I couldn't come up with anything better. Closes: https://github.com/davishmcclurg/json_schemer/issues/137 [0]: https://json-schema.org/draft/2020-12/json-schema-validation#section-8.1 [1]: https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.8.2 --- README.md | 22 ++++++++ lib/json_schemer.rb | 11 ++++ lib/json_schemer/content.rb | 18 ++++++ lib/json_schemer/draft201909/meta.rb | 2 + lib/json_schemer/draft202012/meta.rb | 6 ++ lib/json_schemer/draft202012/vocab/content.rb | 12 +++- lib/json_schemer/draft4/meta.rb | 2 + lib/json_schemer/draft6/meta.rb | 2 + lib/json_schemer/draft7/meta.rb | 2 + lib/json_schemer/draft7/vocab/validation.rb | 8 +-- lib/json_schemer/format.rb | 26 --------- lib/json_schemer/openapi30/meta.rb | 2 +- lib/json_schemer/schema.rb | 26 ++++++++- test/json_schemer_test.rb | 56 ++++++++++++++++++- 14 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 lib/json_schemer/content.rb diff --git a/README.md b/README.md index b119fa6a..2233a219 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,28 @@ JSONSchemer.schema( # default: true format: true, + # custom content encodings + # only `base64` is available by default + content_encodings: { + # return [success, annotation] tuple + 'urlsafe_base64' => proc do |instance| + [true, Base64.urlsafe_decode64(instance)] + rescue + [false, nil] + end + }, + + # custom content media types + # only `application/json` is available by default + content_media_types: { + # return [success, annotation] tuple + 'text/csv' => proc do |instance| + [true, CSV.parse(instance)] + rescue + [false, nil] + end + }, + # insert default property values during validation # true/false # default: false diff --git a/lib/json_schemer.rb b/lib/json_schemer.rb index ecce26d7..49ad1449 100644 --- a/lib/json_schemer.rb +++ b/lib/json_schemer.rb @@ -20,6 +20,7 @@ require 'json_schemer/format/uri_template' require 'json_schemer/format/email' require 'json_schemer/format' +require 'json_schemer/content' require 'json_schemer/errors' require 'json_schemer/cached_resolver' require 'json_schemer/ecma_regexp' @@ -146,6 +147,8 @@ def draft202012 Draft202012::SCHEMA, :base_uri => Draft202012::BASE_URI, :formats => Draft202012::FORMATS, + :content_encodings => Draft202012::CONTENT_ENCODINGS, + :content_media_types => Draft202012::CONTENT_MEDIA_TYPES, :ref_resolver => Draft202012::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -156,6 +159,8 @@ def draft201909 Draft201909::SCHEMA, :base_uri => Draft201909::BASE_URI, :formats => Draft201909::FORMATS, + :content_encodings => Draft201909::CONTENT_ENCODINGS, + :content_media_types => Draft201909::CONTENT_MEDIA_TYPES, :ref_resolver => Draft201909::Meta::SCHEMAS.to_proc, :regexp_resolver => 'ecma' ) @@ -167,6 +172,8 @@ def draft7 :vocabulary => { 'json-schemer://draft7' => true }, :base_uri => Draft7::BASE_URI, :formats => Draft7::FORMATS, + :content_encodings => Draft7::CONTENT_ENCODINGS, + :content_media_types => Draft7::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end @@ -177,6 +184,8 @@ def draft6 :vocabulary => { 'json-schemer://draft6' => true }, :base_uri => Draft6::BASE_URI, :formats => Draft6::FORMATS, + :content_encodings => Draft6::CONTENT_ENCODINGS, + :content_media_types => Draft6::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end @@ -187,6 +196,8 @@ def draft4 :vocabulary => { 'json-schemer://draft4' => true }, :base_uri => Draft4::BASE_URI, :formats => Draft4::FORMATS, + :content_encodings => Draft4::CONTENT_ENCODINGS, + :content_media_types => Draft4::CONTENT_MEDIA_TYPES, :regexp_resolver => 'ecma' ) end diff --git a/lib/json_schemer/content.rb b/lib/json_schemer/content.rb new file mode 100644 index 00000000..40ac3282 --- /dev/null +++ b/lib/json_schemer/content.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module JSONSchemer + module ContentEncoding + BASE64 = proc do |instance| + [true, Base64.strict_decode64(instance)] + rescue + [false, nil] + end + end + + module ContentMediaType + JSON = proc do |instance| + [true, ::JSON.parse(instance)] + rescue + [false, nil] + end + end +end diff --git a/lib/json_schemer/draft201909/meta.rb b/lib/json_schemer/draft201909/meta.rb index daeced90..360113d3 100644 --- a/lib/json_schemer/draft201909/meta.rb +++ b/lib/json_schemer/draft201909/meta.rb @@ -3,6 +3,8 @@ module JSONSchemer module Draft201909 BASE_URI = URI('https://json-schema.org/draft/2019-09/schema') FORMATS = Draft202012::FORMATS + CONTENT_ENCODINGS = Draft202012::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft202012::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/schema', diff --git a/lib/json_schemer/draft202012/meta.rb b/lib/json_schemer/draft202012/meta.rb index 14e47c81..c0e02e7d 100644 --- a/lib/json_schemer/draft202012/meta.rb +++ b/lib/json_schemer/draft202012/meta.rb @@ -23,6 +23,12 @@ module Draft202012 'relative-json-pointer' => Format::RELATIVE_JSON_POINTER, 'regex' => Format::REGEX } + CONTENT_ENCODINGS = { + 'base64' => ContentEncoding::BASE64 + } + CONTENT_MEDIA_TYPES = { + 'application/json' => ContentMediaType::JSON + } SCHEMA = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/schema', diff --git a/lib/json_schemer/draft202012/vocab/content.rb b/lib/json_schemer/draft202012/vocab/content.rb index abdefd95..8bfbbd75 100644 --- a/lib/json_schemer/draft202012/vocab/content.rb +++ b/lib/json_schemer/draft202012/vocab/content.rb @@ -4,21 +4,29 @@ module Draft202012 module Vocab module Content class ContentEncoding < Keyword + def parse + root.fetch_content_encoding(value) { raise UnknownContentEncoding, value } + end + def validate(instance, instance_location, keyword_location, _context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) - _valid, annotation = Format.decode_content_encoding(instance, value) + _valid, annotation = parsed.call(instance) result(instance, instance_location, keyword_location, true, :annotation => annotation) end end class ContentMediaType < Keyword + def parse + root.fetch_content_media_type(value) { raise UnknownContentMediaType, value } + end + def validate(instance, instance_location, keyword_location, context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance - _valid, annotation = Format.parse_content_media_type(decoded_instance, value) + _valid, annotation = parsed.call(decoded_instance) result(instance, instance_location, keyword_location, true, :annotation => annotation) end diff --git a/lib/json_schemer/draft4/meta.rb b/lib/json_schemer/draft4/meta.rb index 10fc977b..14d9c27b 100644 --- a/lib/json_schemer/draft4/meta.rb +++ b/lib/json_schemer/draft4/meta.rb @@ -6,6 +6,8 @@ module Draft4 FORMATS.delete('uri-reference') FORMATS.delete('uri-template') FORMATS.delete('json-pointer') + CONTENT_ENCODINGS = Draft6::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft6::CONTENT_MEDIA_TYPES SCHEMA = { 'id' => 'http://json-schema.org/draft-04/schema#', '$schema' => 'http://json-schema.org/draft-04/schema#', diff --git a/lib/json_schemer/draft6/meta.rb b/lib/json_schemer/draft6/meta.rb index 5fb958e0..502a0268 100644 --- a/lib/json_schemer/draft6/meta.rb +++ b/lib/json_schemer/draft6/meta.rb @@ -11,6 +11,8 @@ module Draft6 FORMATS.delete('iri-reference') FORMATS.delete('relative-json-pointer') FORMATS.delete('regex') + CONTENT_ENCODINGS = Draft7::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft7::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'http://json-schema.org/draft-06/schema#', '$id' => 'http://json-schema.org/draft-06/schema#', diff --git a/lib/json_schemer/draft7/meta.rb b/lib/json_schemer/draft7/meta.rb index 34396da9..832aa857 100644 --- a/lib/json_schemer/draft7/meta.rb +++ b/lib/json_schemer/draft7/meta.rb @@ -5,6 +5,8 @@ module Draft7 FORMATS = Draft201909::FORMATS.dup FORMATS.delete('duration') FORMATS.delete('uuid') + CONTENT_ENCODINGS = Draft201909::CONTENT_ENCODINGS + CONTENT_MEDIA_TYPES = Draft201909::CONTENT_MEDIA_TYPES SCHEMA = { '$schema' => 'http://json-schema.org/draft-07/schema#', '$id' => 'http://json-schema.org/draft-07/schema#', diff --git a/lib/json_schemer/draft7/vocab/validation.rb b/lib/json_schemer/draft7/vocab/validation.rb index c7df92ef..f38f0810 100644 --- a/lib/json_schemer/draft7/vocab/validation.rb +++ b/lib/json_schemer/draft7/vocab/validation.rb @@ -35,7 +35,7 @@ def validate(instance, instance_location, keyword_location, context) end end - class ContentEncoding < Keyword + class ContentEncoding < Draft202012::Vocab::Content::ContentEncoding def error(formatted_instance_location:, **) "string at #{formatted_instance_location} could not be decoded using encoding: #{value}" end @@ -43,13 +43,13 @@ def error(formatted_instance_location:, **) def validate(instance, instance_location, keyword_location, _context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) - valid, annotation = Format.decode_content_encoding(instance, value) + valid, annotation = parsed.call(instance) result(instance, instance_location, keyword_location, valid, :annotation => annotation) end end - class ContentMediaType < Keyword + class ContentMediaType < Draft202012::Vocab::Content::ContentMediaType def error(formatted_instance_location:, **) "string at #{formatted_instance_location} could not be parsed using media type: #{value}" end @@ -58,7 +58,7 @@ def validate(instance, instance_location, keyword_location, context) return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String) decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance - valid, annotation = Format.parse_content_media_type(decoded_instance, value) + valid, annotation = parsed.call(decoded_instance) result(instance, instance_location, keyword_location, valid, :annotation => annotation) end diff --git a/lib/json_schemer/format.rb b/lib/json_schemer/format.rb index c11e339e..fa96cc28 100644 --- a/lib/json_schemer/format.rb +++ b/lib/json_schemer/format.rb @@ -93,32 +93,6 @@ def percent_encode(data, regexp) data.force_encoding(Encoding::US_ASCII) end - def decode_content_encoding(data, content_encoding) - case content_encoding - when 'base64' - begin - [true, Base64.strict_decode64(data)] - rescue - [false, nil] - end - else - raise UnknownContentEncoding, content_encoding - end - end - - def parse_content_media_type(data, content_media_type) - case content_media_type - when 'application/json' - begin - [true, JSON.parse(data)] - rescue - [false, nil] - end - else - raise UnknownContentMediaType, content_media_type - end - end - def valid_date_time?(data) return false if HOUR_24_REGEX.match?(data) datetime = DateTime.rfc3339(data) diff --git a/lib/json_schemer/openapi30/meta.rb b/lib/json_schemer/openapi30/meta.rb index c5bcf478..4b62eb86 100644 --- a/lib/json_schemer/openapi30/meta.rb +++ b/lib/json_schemer/openapi30/meta.rb @@ -4,7 +4,7 @@ module OpenAPI30 BASE_URI = URI('json-schemer://openapi30/schema') # https://spec.openapis.org/oas/v3.0.3#data-types FORMATS = OpenAPI31::FORMATS.merge( - 'byte' => proc { |instance, _value| Format.decode_content_encoding(instance, 'base64').first }, + 'byte' => proc { |instance, _value| ContentEncoding::BASE64.call(instance).first }, 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT }, 'date' => Format::DATE ) diff --git a/lib/json_schemer/schema.rb b/lib/json_schemer/schema.rb index 4b760fd0..1c2985bb 100644 --- a/lib/json_schemer/schema.rb +++ b/lib/json_schemer/schema.rb @@ -20,6 +20,8 @@ def original_instance(instance_location) PROPERTIES_KEYWORD_CLASS = Draft202012::Vocab::Applicator::Properties DEFAULT_BASE_URI = URI('json-schemer://schema').freeze DEFAULT_FORMATS = {}.freeze + DEFAULT_CONTENT_ENCODINGS = {}.freeze + DEFAULT_CONTENT_MEDIA_TYPES = {}.freeze DEFAULT_KEYWORDS = {}.freeze DEFAULT_BEFORE_PROPERTY_VALIDATION = [].freeze DEFAULT_AFTER_PROPERTY_VALIDATION = [].freeze @@ -41,7 +43,7 @@ def original_instance(instance_location) attr_accessor :base_uri, :meta_schema, :keywords, :keyword_order attr_reader :value, :parent, :root, :parsed - attr_reader :vocabulary, :format, :formats, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver + attr_reader :vocabulary, :format, :formats, :content_encodings, :content_media_types, :custom_keywords, :before_property_validation, :after_property_validation, :insert_property_defaults, :property_default_resolver def initialize( value, @@ -53,6 +55,8 @@ def initialize( vocabulary: nil, format: true, formats: DEFAULT_FORMATS, + content_encodings: DEFAULT_CONTENT_ENCODINGS, + content_media_types: DEFAULT_CONTENT_MEDIA_TYPES, keywords: DEFAULT_KEYWORDS, before_property_validation: DEFAULT_BEFORE_PROPERTY_VALIDATION, after_property_validation: DEFAULT_AFTER_PROPERTY_VALIDATION, @@ -74,6 +78,8 @@ def initialize( @vocabulary = vocabulary @format = format @formats = formats + @content_encodings = content_encodings + @content_media_types = content_media_types @custom_keywords = keywords @before_property_validation = Array(before_property_validation) @after_property_validation = Array(after_property_validation) @@ -182,6 +188,8 @@ def resolve_ref(uri) :meta_schema => meta_schema, :format => format, :formats => formats, + :content_encodings => content_encodings, + :content_media_types => content_media_types, :keywords => custom_keywords, :before_property_validation => before_property_validation, :after_property_validation => after_property_validation, @@ -295,6 +303,22 @@ def fetch_format(format, *args, &block) end end + def fetch_content_encoding(content_encoding, *args, &block) + if meta_schema == self + content_encodings.fetch(content_encoding, *args, &block) + else + content_encodings.fetch(content_encoding) { meta_schema.fetch_content_encoding(content_encoding, *args, &block) } + end + end + + def fetch_content_media_type(content_media_type, *args, &block) + if meta_schema == self + content_media_types.fetch(content_media_type, *args, &block) + else + content_media_types.fetch(content_media_type) { meta_schema.fetch_content_media_type(content_media_type, *args, &block) } + end + end + def id_keyword @id_keyword ||= (keywords.key?('$id') ? '$id' : 'id') end diff --git a/test/json_schemer_test.rb b/test/json_schemer_test.rb index bfa1b012..346ba11c 100644 --- a/test/json_schemer_test.rb +++ b/test/json_schemer_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'csv' class JSONSchemerTest < Minitest::Test def test_that_it_has_a_version_number @@ -237,11 +238,11 @@ def test_it_ignores_invalid_types end def test_it_raises_for_unsupported_content_encoding - assert_raises(JSONSchemer::UnknownContentEncoding) { JSONSchemer.schema({ 'contentEncoding' => '7bit' }).valid?('') } + assert_raises(JSONSchemer::UnknownContentEncoding) { JSONSchemer.schema({ 'contentEncoding' => '7bit' }) } end def test_it_raises_for_unsupported_content_media_type - assert_raises(JSONSchemer::UnknownContentMediaType) { JSONSchemer.schema({ 'contentMediaType' => 'application/xml' }).valid?('') } + assert_raises(JSONSchemer::UnknownContentMediaType) { JSONSchemer.schema({ 'contentMediaType' => 'application/xml' }) } end def test_it_raises_for_required_unknown_vocabulary @@ -547,4 +548,55 @@ def test_bundle_exclusive_ref assert(bundle.valid?('yah')) refute(bundle.valid?('nah')) end + + def test_custom_content_encodings_and_media_types + data = '😊' + instance = Base64.urlsafe_encode64(data) + schema = { + 'contentEncoding' => 'urlsafe_base64', + 'contentMediaType' => 'text/csv' + } + content_encodings = { + 'urlsafe_base64' => proc do |instance| + [true, Base64.urlsafe_decode64(instance).force_encoding('utf-8')] + rescue + [false, nil] + end + } + content_media_types = { + 'text/csv' => proc do |instance| + [true, CSV.parse(instance)] + rescue + [false, nil] + end + } + + refute(JSONSchemer.schema({ 'contentEncoding' => 'base64' }).validate(instance, :output_format => 'basic').fetch('annotations').first.key?('annotation')) + + schemer = JSONSchemer.schema(schema, :content_encodings => content_encodings, :content_media_types => content_media_types) + + assert_nil(annotation(schemer.validate('invalid', :output_format => 'basic'), '/contentEncoding')) + assert_nil(annotation(schemer.validate(Base64.urlsafe_encode64("#{data}\""), :output_format => 'basic'), '/contentMediaType')) + + result = schemer.validate(instance, :output_format => 'basic') + assert_equal(data, annotation(result, '/contentEncoding')) + assert_equal([[data]], annotation(result, '/contentMediaType')) + + draft7_schemer = JSONSchemer.schema( + schema, + :meta_schema => JSONSchemer.draft7, + :content_encodings => content_encodings, + :content_media_types => content_media_types + ) + + assert(draft7_schemer.valid?(instance)) + refute(draft7_schemer.valid?('invalid')) + refute(draft7_schemer.valid?(Base64.urlsafe_encode64("#{data}\""))) + end + +private + + def annotation(result, keyword_location) + result.fetch('annotations').find { |annotation| annotation.fetch('keywordLocation') == keyword_location }['annotation'] + end end From 3ab85adc134a2b8015f76464867ba2eff148b221 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Sat, 14 Oct 2023 13:23:30 -0700 Subject: [PATCH 13/18] Document custom formats This behavior has been around but undocumented for a while. I'm adding this now because it's similar to custom content encodings and media types. Related: https://github.com/davishmcclurg/json_schemer/issues/137 --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 2233a219..c2729d4a 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,15 @@ JSONSchemer.schema( # default: true format: true, + # custom formats + formats: { + 'int32' => proc do |instance, _format| + instance.is_a?(Integer) && instance.bit_length <= 32 + end, + # disable specific format + 'email' => false + }, + # custom content encodings # only `base64` is available by default content_encodings: { From daf4fce5086a1515c5882f10371f280d6eae2fb0 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Wed, 1 Nov 2023 15:11:55 -0700 Subject: [PATCH 14/18] Allow space separator in `date-time` regexes [RFC 3339][0] is not very clear about what separator characters are allowed. It just lists "space" as an example: > NOTE: ISO 8601 defines date and time separated by "T". > Applications using this syntax may choose, for the sake of > readability, to specify a full-date and full-time separated by > (say) a space character. Ruby's [`DateTime.rfc3339`][1] allows `T`, `t`, and `\s`, so I'm going with that. This should only affect the edge case tests since everything else is delegated to `DateTime.rfc3339`. There are a bunch of tests that were generated here: https://ijmacd.github.io/rfc3339-iso8601/ Thanks to @pboling for pointing me to it in: https://github.com/davishmcclurg/json_schemer/issues/153 The RFC 3339 examples include some that use an underscore as the date-time separator, which I decided to ignore because it seems arbitrary to allow random characters and Ruby doesn't support it. [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 [1]: https://github.com/ruby/ruby/blob/d3ea9070bbbf04749e5fcd8339d71a9e73a86cfb/ext/date/date_parse.c#L2613 --- lib/json_schemer/format.rb | 5 +- test/format_test.rb | 628 +++++++++++++++++++++++++++++++++++++ 2 files changed, 631 insertions(+), 2 deletions(-) diff --git a/lib/json_schemer/format.rb b/lib/json_schemer/format.rb index c11e339e..43b0f272 100644 --- a/lib/json_schemer/format.rb +++ b/lib/json_schemer/format.rb @@ -68,8 +68,9 @@ module Format end DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze - HOUR_24_REGEX = /T24/.freeze - LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze + DATE_TIME_SEPARATOR_CHARACTER_CLASS = '[Tt\s]' + HOUR_24_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}24:/.freeze + LEAP_SECOND_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}\d{2}:\d{2}:6/.freeze IP_REGEX = /\A[\h:.]+\z/.freeze INVALID_QUERY_REGEX = /\s/.freeze IRI_ESCAPE_REGEX = /[^[:ascii:]]/ diff --git a/test/format_test.rb b/test/format_test.rb index a1deec45..613e5240 100644 --- a/test/format_test.rb +++ b/test/format_test.rb @@ -113,4 +113,632 @@ def test_email_format assert_equal(valid, schema.valid?(email)) end end + + def test_rfc3339_vs_iso8601 + # generated via: https://ijmacd.github.io/rfc3339-iso8601/ + rfc3339_dates = Set[ + '2023-11-01' + ] + rfc3339_times = Set[ + '11:24:25-07:00', + '11:24:25.5-07:00', + '11:24:25.50-07:00', + '11:24:25.500-07:00', + '11:24:25.500623-07:00', + '18:24:25Z', + '18:24:25.5Z', + '18:24:25.50Z', + '18:24:25.500Z', + '18:24:25.500623Z', + '18:24:25+00:00', + '18:24:25.5+00:00', + '18:24:25.500+00:00', + '18:24:25.500623+00:00', + '18:24:25-00:00', + '18:24:25.5-00:00', + '18:24:25.500-00:00', + '18:24:25.500623-00:00' + ] + rfc3339_date_times = Set[ + '2023-11-01T18:24:25Z', + '2023-11-01T18:24:25.5Z', + '2023-11-01T18:24:25.50Z', + '2023-11-01T18:24:25.500Z', + '2023-11-01T18:24:25.500623Z', + '2023-11-01t18:24:25z', + '2023-11-01t18:24:25.500z', + '2023-11-01T11:24:25-07:00', + '2023-11-01T11:24:25.5-07:00', + '2023-11-01T11:24:25.50-07:00', + '2023-11-01T11:24:25.500-07:00', + '2023-11-01T11:24:25.500623-07:00', + '2023-11-01 11:24:25-07:00', + '2023-11-01 11:24:25.5-07:00', + '2023-11-01 11:24:25.50-07:00', + '2023-11-01 11:24:25.500-07:00', + '2023-11-01 11:24:25.500623-07:00', + '2023-11-01 18:24:25Z', + # '2023-11-01_18:24:25Z', + '2023-11-01 18:24:25z', + # '2023-11-01_18:24:25z', + '2023-11-01 18:24:25.5Z', + '2023-11-01 18:24:25.50Z', + '2023-11-01 18:24:25.500Z', + # '2023-11-01_18:24:25.500Z', + '2023-11-01 18:24:25.500623Z', + # '2023-11-01_18:24:25.500623Z', + '2023-11-01 18:24:25.500z', + # '2023-11-01_18:24:25.500z', + '2023-11-01 18:24:25.500623z', + # '2023-11-01_18:24:25.500623z', + '2023-11-01 18:24:25-00:00', + '2023-11-01 18:24:25.500-00:00', + '2023-11-01T18:24:25-00:00', + '2023-11-01T18:24:25.500-00:00', + '2023-11-02T03:09:25+08:45', + '2023-11-01T18:24:25+00:00', + '2023-11-01T18:24:25.500+00:00' + ] + iso8601_dates = Set[ + '2023-11-01', + '20', + '202', + '2023', + '2023-11', + '2023-305', + '2023-W44', + '2023-W44-3', + '20231101', + '2023305', + '2023W44', + '2023W443' + ] + iso8601_times = Set[ + '11:24:25-07:00', + '11:24:25.5-07:00', + '11:24:25.50-07:00', + '11:24:25.500-07:00', + '11:24:25.500623-07:00', + '18:24:25Z', + '18:24:25.5Z', + '18:24:25.50Z', + '18:24:25.500Z', + '18:24:25.500623Z', + '18:24:25+00:00', + '18:24:25.5+00:00', + '18:24:25.500+00:00', + '18:24:25.500623+00:00', + '11', + '11,4', + '11.4', + '11:24', + '11:24,4', + '11:24.4', + '11:24:25', + '11:24:25.5', + '11:24:25.50', + '11:24:25,500', + '11:24:25.500', + '11:24:25,500623', + '11:24:25.500623', + '18Z', + '18,4Z', + '18.4Z', + '18:24Z', + '18:24,4Z', + '18:24.4Z', + '18:24:25,500Z', + '18:24:25,500623Z', + '11-07', + '11,4-07', + '11.4-07', + '11:24-07', + '11:24,4-07', + '11:24.4-07', + '11:24:25-07', + '11:24:25.5-07', + '11:24:25.50-07', + '11:24:25,500-07', + '11:24:25.500-07', + '11:24:25,500623-07', + '11:24:25.500623-07', + '11-07:00', + '11,4-07:00', + '11.4-07:00', + '11:24-07:00', + '11:24,4-07:00', + '11:24.4-07:00', + '11:24:25,500-07:00', + '11:24:25,500623-07:00', + 'T11', + 'T11,4', + 'T11.4', + 'T11:24', + 'T11:24,4', + 'T11:24.4', + 'T11:24:25', + 'T11:24:25.5', + 'T11:24:25.50', + 'T11:24:25,500', + 'T11:24:25.500', + 'T11:24:25,500623', + 'T11:24:25.500623', + 'T18Z', + 'T18,4Z', + 'T18.4Z', + 'T18:24Z', + 'T18:24,4Z', + 'T18:24.4Z', + 'T18:24:25Z', + 'T18:24:25.5Z', + 'T18:24:25.50Z', + 'T18:24:25,500Z', + 'T18:24:25.500Z', + 'T18:24:25,500623Z', + 'T18:24:25.500623Z', + 'T11-07', + 'T11,4-07', + 'T11.4-07', + 'T11:24-07', + 'T11:24,4-07', + 'T11:24.4-07', + 'T11:24:25-07', + 'T11:24:25.5-07', + 'T11:24:25.50-07', + 'T11:24:25,500-07', + 'T11:24:25.500-07', + 'T11:24:25,500623-07', + 'T11:24:25.500623-07', + 'T11-07:00', + 'T11,4-07:00', + 'T11.4-07:00', + 'T11:24-07:00', + 'T11:24,4-07:00', + 'T11:24.4-07:00', + 'T11:24:25-07:00', + 'T11:24:25.5-07:00', + 'T11:24:25.50-07:00', + 'T11:24:25,500-07:00', + 'T11:24:25.500-07:00', + 'T11:24:25,500623-07:00', + 'T11:24:25.500623-07:00', + '1124', + '1124,4', + '1124.4', + '112425', + '112425.5', + '112425.50', + '112425,500', + '112425.500', + '112425,500623', + '112425.500623', + '1824Z', + '1824,4Z', + '1824.4Z', + '182425Z', + '182425.5Z', + '182425.50Z', + '182425,500Z', + '182425.500Z', + '182425,500623Z', + '182425.500623Z', + '1124-07', + '1124,4-07', + '1124.4-07', + '112425-07', + '112425.5-07', + '112425.50-07', + '112425,500-07', + '112425.500-07', + '112425,500623-07', + '112425.500623-07', + '11-0700', + '11,4-0700', + '11.4-0700', + '1124-0700', + '1124,4-0700', + '1124.4-0700', + '112425-0700', + '112425.5-0700', + '112425.50-0700', + '112425,500-0700', + '112425.500-0700', + '112425,500623-0700', + '112425.500623-0700', + 'T1124', + 'T1124,4', + 'T1124.4', + 'T112425', + 'T112425.5', + 'T112425.50', + 'T112425,500', + 'T112425.500', + 'T112425,500623', + 'T112425.500623', + 'T1824Z', + 'T1824,4Z', + 'T1824.4Z', + 'T182425Z', + 'T182425.5Z', + 'T182425.50Z', + 'T182425,500Z', + 'T182425.500Z', + 'T182425,500623Z', + 'T182425.500623Z', + 'T1124-07', + 'T1124,4-07', + 'T1124.4-07', + 'T112425-07', + 'T112425.5-07', + 'T112425.50-07', + 'T112425,500-07', + 'T112425.500-07', + 'T112425,500623-07', + 'T112425.500623-07', + 'T11-0700', + 'T11,4-0700', + 'T11.4-0700', + 'T1124-0700', + 'T1124,4-0700', + 'T1124.4-0700', + 'T112425-0700', + 'T112425.5-0700', + 'T112425.50-0700', + 'T112425,500-0700', + 'T112425.500-0700', + 'T112425,500623-0700', + 'T112425.500623-0700' + ] + iso8601_date_times = Set[ + '2023-11-01T18:24:25Z', + '2023-11-01T18:24:25.5Z', + '2023-11-01T18:24:25.50Z', + '2023-11-01T18:24:25.500Z', + '2023-11-01T18:24:25.500623Z', + '2023-11-01T11:24:25-07:00', + '2023-11-01T11:24:25.500-07:00', + '2023-11-01T11:24:25.500623-07:00', + '2023-11-02T03:09:25+08:45', + '2023-11-01T18:24:25+00:00', + '2023-11-01T18:24:25.500+00:00', + '2023-11-01T11', + '2023-11-01T11,4', + '2023-11-01T11.4', + '2023-11-01T11:24', + '2023-11-01T11:24,4', + '2023-11-01T11:24.4', + '2023-11-01T11:24:25', + '2023-11-01T11:24:25.5', + '2023-11-01T11:24:25.50', + '2023-11-01T11:24:25,500', + '2023-11-01T11:24:25.500', + '2023-11-01T11:24:25,500623', + '2023-11-01T11:24:25.500623', + '2023-11-01T18Z', + '2023-11-01T18,4Z', + '2023-11-01T18.4Z', + '2023-11-01T18:24Z', + '2023-11-01T18:24,4Z', + '2023-11-01T18:24.4Z', + '2023-11-01T18:24:25,500Z', + '2023-11-01T18:24:25,500623Z', + '2023-11-01T11-07', + '2023-11-01T11,4-07', + '2023-11-01T11.4-07', + '2023-11-01T11:24-07', + '2023-11-01T11:24,4-07', + '2023-11-01T11:24.4-07', + '2023-11-01T11:24:25-07', + '2023-11-01T11:24:25.5-07', + '2023-11-01T11:24:25.50-07', + '2023-11-01T11:24:25,500-07', + '2023-11-01T11:24:25.500-07', + '2023-11-01T11:24:25,500623-07', + '2023-11-01T11:24:25.500623-07', + '2023-11-01T11-07:00', + '2023-11-01T11,4-07:00', + '2023-11-01T11.4-07:00', + '2023-11-01T11:24-07:00', + '2023-11-01T11:24,4-07:00', + '2023-11-01T11:24.4-07:00', + '2023-11-01T11:24:25.5-07:00', + '2023-11-01T11:24:25.50-07:00', + '2023-11-01T11:24:25,500-07:00', + '2023-11-01T11:24:25,500623-07:00', + '2023-W44-3T11', + '2023-W44-3T11,4', + '2023-W44-3T11.4', + '2023-W44-3T11:24', + '2023-W44-3T11:24,4', + '2023-W44-3T11:24.4', + '2023-W44-3T11:24:25', + '2023-W44-3T11:24:25.5', + '2023-W44-3T11:24:25.50', + '2023-W44-3T11:24:25,500', + '2023-W44-3T11:24:25.500', + '2023-W44-3T11:24:25,500623', + '2023-W44-3T11:24:25.500623', + '2023-W44-3T18Z', + '2023-W44-3T18,4Z', + '2023-W44-3T18.4Z', + '2023-W44-3T18:24Z', + '2023-W44-3T18:24,4Z', + '2023-W44-3T18:24.4Z', + '2023-W44-3T18:24:25Z', + '2023-W44-3T18:24:25.5Z', + '2023-W44-3T18:24:25.50Z', + '2023-W44-3T18:24:25,500Z', + '2023-W44-3T18:24:25.500Z', + '2023-W44-3T18:24:25,500623Z', + '2023-W44-3T18:24:25.500623Z', + '2023-W44-3T11-07', + '2023-W44-3T11,4-07', + '2023-W44-3T11.4-07', + '2023-W44-3T11:24-07', + '2023-W44-3T11:24,4-07', + '2023-W44-3T11:24.4-07', + '2023-W44-3T11:24:25-07', + '2023-W44-3T11:24:25.5-07', + '2023-W44-3T11:24:25.50-07', + '2023-W44-3T11:24:25,500-07', + '2023-W44-3T11:24:25.500-07', + '2023-W44-3T11:24:25,500623-07', + '2023-W44-3T11:24:25.500623-07', + '2023-W44-3T11-07:00', + '2023-W44-3T11,4-07:00', + '2023-W44-3T11.4-07:00', + '2023-W44-3T11:24-07:00', + '2023-W44-3T11:24,4-07:00', + '2023-W44-3T11:24.4-07:00', + '2023-W44-3T11:24:25-07:00', + '2023-W44-3T11:24:25.5-07:00', + '2023-W44-3T11:24:25.50-07:00', + '2023-W44-3T11:24:25,500-07:00', + '2023-W44-3T11:24:25.500-07:00', + '2023-W44-3T11:24:25,500623-07:00', + '2023-W44-3T11:24:25.500623-07:00', + '2023-305T11', + '2023-305T11,4', + '2023-305T11.4', + '2023-305T11:24', + '2023-305T11:24,4', + '2023-305T11:24.4', + '2023-305T11:24:25', + '2023-305T11:24:25.5', + '2023-305T11:24:25.50', + '2023-305T11:24:25,500', + '2023-305T11:24:25.500', + '2023-305T11:24:25,500623', + '2023-305T11:24:25.500623', + '2023-305T18Z', + '2023-305T18,4Z', + '2023-305T18.4Z', + '2023-305T18:24Z', + '2023-305T18:24,4Z', + '2023-305T18:24.4Z', + '2023-305T18:24:25Z', + '2023-305T18:24:25.5Z', + '2023-305T18:24:25.50Z', + '2023-305T18:24:25,500Z', + '2023-305T18:24:25.500Z', + '2023-305T18:24:25,500623Z', + '2023-305T18:24:25.500623Z', + '2023-305T11-07', + '2023-305T11,4-07', + '2023-305T11.4-07', + '2023-305T11:24-07', + '2023-305T11:24,4-07', + '2023-305T11:24.4-07', + '2023-305T11:24:25-07', + '2023-305T11:24:25.5-07', + '2023-305T11:24:25.50-07', + '2023-305T11:24:25,500-07', + '2023-305T11:24:25.500-07', + '2023-305T11:24:25,500623-07', + '2023-305T11:24:25.500623-07', + '2023-305T11-07:00', + '2023-305T11,4-07:00', + '2023-305T11.4-07:00', + '2023-305T11:24-07:00', + '2023-305T11:24,4-07:00', + '2023-305T11:24.4-07:00', + '2023-305T11:24:25-07:00', + '2023-305T11:24:25.5-07:00', + '2023-305T11:24:25.50-07:00', + '2023-305T11:24:25,500-07:00', + '2023-305T11:24:25.500-07:00', + '2023-305T11:24:25,500623-07:00', + '2023-305T11:24:25.500623-07:00', + '20231101T11', + '20231101T11,4', + '20231101T11.4', + '20231101T1124', + '20231101T1124,4', + '20231101T1124.4', + '20231101T112425', + '20231101T112425.5', + '20231101T112425.50', + '20231101T112425,500', + '20231101T112425.500', + '20231101T112425,500623', + '20231101T112425.500623', + '20231101T18Z', + '20231101T18,4Z', + '20231101T18.4Z', + '20231101T1824Z', + '20231101T1824,4Z', + '20231101T1824.4Z', + '20231101T182425Z', + '20231101T182425.5Z', + '20231101T182425.50Z', + '20231101T182425,500Z', + '20231101T182425.500Z', + '20231101T182425,500623Z', + '20231101T182425.500623Z', + '20231101T11-07', + '20231101T11,4-07', + '20231101T11.4-07', + '20231101T1124-07', + '20231101T1124,4-07', + '20231101T1124.4-07', + '20231101T112425-07', + '20231101T112425.5-07', + '20231101T112425.50-07', + '20231101T112425,500-07', + '20231101T112425.500-07', + '20231101T112425,500623-07', + '20231101T112425.500623-07', + '20231101T11-0700', + '20231101T11,4-0700', + '20231101T11.4-0700', + '20231101T1124-0700', + '20231101T1124,4-0700', + '20231101T1124.4-0700', + '20231101T112425-0700', + '20231101T112425.5-0700', + '20231101T112425.50-0700', + '20231101T112425,500-0700', + '20231101T112425.500-0700', + '20231101T112425,500623-0700', + '20231101T112425.500623-0700', + '2023W443T11', + '2023W443T11,4', + '2023W443T11.4', + '2023W443T1124', + '2023W443T1124,4', + '2023W443T1124.4', + '2023W443T112425', + '2023W443T112425.5', + '2023W443T112425.50', + '2023W443T112425,500', + '2023W443T112425.500', + '2023W443T112425,500623', + '2023W443T112425.500623', + '2023W443T18Z', + '2023W443T18,4Z', + '2023W443T18.4Z', + '2023W443T1824Z', + '2023W443T1824,4Z', + '2023W443T1824.4Z', + '2023W443T182425Z', + '2023W443T182425.5Z', + '2023W443T182425.50Z', + '2023W443T182425,500Z', + '2023W443T182425.500Z', + '2023W443T182425,500623Z', + '2023W443T182425.500623Z', + '2023W443T11-07', + '2023W443T11,4-07', + '2023W443T11.4-07', + '2023W443T1124-07', + '2023W443T1124,4-07', + '2023W443T1124.4-07', + '2023W443T112425-07', + '2023W443T112425.5-07', + '2023W443T112425.50-07', + '2023W443T112425,500-07', + '2023W443T112425.500-07', + '2023W443T112425,500623-07', + '2023W443T112425.500623-07', + '2023W443T11-0700', + '2023W443T11,4-0700', + '2023W443T11.4-0700', + '2023W443T1124-0700', + '2023W443T1124,4-0700', + '2023W443T1124.4-0700', + '2023W443T112425-0700', + '2023W443T112425.5-0700', + '2023W443T112425.50-0700', + '2023W443T112425,500-0700', + '2023W443T112425.500-0700', + '2023W443T112425,500623-0700', + '2023W443T112425.500623-0700', + '2023305T11', + '2023305T11,4', + '2023305T11.4', + '2023305T1124', + '2023305T1124,4', + '2023305T1124.4', + '2023305T112425', + '2023305T112425.5', + '2023305T112425.50', + '2023305T112425,500', + '2023305T112425.500', + '2023305T112425,500623', + '2023305T112425.500623', + '2023305T18Z', + '2023305T18,4Z', + '2023305T18.4Z', + '2023305T1824Z', + '2023305T1824,4Z', + '2023305T1824.4Z', + '2023305T182425Z', + '2023305T182425.5Z', + '2023305T182425.50Z', + '2023305T182425,500Z', + '2023305T182425.500Z', + '2023305T182425,500623Z', + '2023305T182425.500623Z', + '2023305T11-07', + '2023305T11,4-07', + '2023305T11.4-07', + '2023305T1124-07', + '2023305T1124,4-07', + '2023305T1124.4-07', + '2023305T112425-07', + '2023305T112425.5-07', + '2023305T112425.50-07', + '2023305T112425,500-07', + '2023305T112425.500-07', + '2023305T112425,500623-07', + '2023305T112425.500623-07', + '2023305T11-0700', + '2023305T11,4-0700', + '2023305T11.4-0700', + '2023305T1124-0700', + '2023305T1124,4-0700', + '2023305T1124.4-0700', + '2023305T112425-0700', + '2023305T112425.5-0700', + '2023305T112425.50-0700', + '2023305T112425,500-0700', + '2023305T112425.500-0700', + '2023305T112425,500623-0700', + '2023305T112425.500623-0700', + '2023-11-02T02:24:25+08', + '2023-11-01T06-12', + '2023-11-01T06-12:00', + '2023-11-01T06:24-12', + '2023-11-01T06:24-12:00', + ] + + rfc3339_dates.each do |date| + assert(JSONSchemer.schema({ 'format' => 'date' }).valid?(date)) + end + rfc3339_times.each do |time| + assert(JSONSchemer.schema({ 'format' => 'time' }).valid?(time)) + end + rfc3339_date_times.each do |date_time| + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?(date_time)) + end + + (iso8601_dates - rfc3339_dates).each do |date| + refute(JSONSchemer.schema({ 'format' => 'date' }).valid?(date)) + end + (iso8601_times - rfc3339_times).each do |time| + refute(JSONSchemer.schema({ 'format' => 'time' }).valid?(time)) + end + (iso8601_date_times - rfc3339_date_times).each do |date_time| + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?(date_time)) + end + end + + def test_date_time_space_separator + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01T23:00:00Z')) + assert(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01 23:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01T24:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('2023-11-01 24:00:00Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('1998-12-31T23:58:60Z')) + refute(JSONSchemer.schema({ 'format' => 'date-time' }).valid?('1998-12-31 23:58:60Z')) + end end From 2c1be1dac00804e33c92bdc1b6e1ba08e2550cfc Mon Sep 17 00:00:00 2001 From: David Harsha Date: Wed, 15 Nov 2023 12:04:54 -0800 Subject: [PATCH 15/18] Remove `$vocabulary` from vocab meta-schemas https://github.com/json-schema-org/json-schema-spec/pull/1460 --- lib/json_schemer/draft201909/meta.rb | 18 ----------- lib/json_schemer/draft202012/meta.rb | 24 --------------- test/fixtures/draft2019-09.json | 42 -------------------------- test/fixtures/draft2020-12.json | 45 ---------------------------- 4 files changed, 129 deletions(-) diff --git a/lib/json_schemer/draft201909/meta.rb b/lib/json_schemer/draft201909/meta.rb index 360113d3..54c6b12e 100644 --- a/lib/json_schemer/draft201909/meta.rb +++ b/lib/json_schemer/draft201909/meta.rb @@ -51,9 +51,6 @@ module Meta CORE = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/core', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/core' => true - }, '$recursiveAnchor' => true, 'title' => 'Core vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -108,9 +105,6 @@ module Meta APPLICATOR = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/applicator', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/applicator' => true - }, '$recursiveAnchor' => true, 'title' => 'Applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -164,9 +158,6 @@ module Meta VALIDATION = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/validation', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/validation' => true - }, '$recursiveAnchor' => true, 'title' => 'Validation vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -262,9 +253,6 @@ module Meta META_DATA = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/meta-data', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/meta-data' => true - }, '$recursiveAnchor' => true, 'title' => 'Meta-data vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -298,9 +286,6 @@ module Meta FORMAT = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/format', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/format' => true - }, '$recursiveAnchor' => true, 'title' => 'Format vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -312,9 +297,6 @@ module Meta CONTENT = { '$schema' => 'https://json-schema.org/draft/2019-09/schema', '$id' => 'https://json-schema.org/draft/2019-09/meta/content', - '$vocabulary' => { - 'https://json-schema.org/draft/2019-09/vocab/content' => true - }, '$recursiveAnchor' => true, 'title' => 'Content vocabulary meta-schema', 'type' => ['object', 'boolean'], diff --git a/lib/json_schemer/draft202012/meta.rb b/lib/json_schemer/draft202012/meta.rb index c0e02e7d..3b1729f6 100644 --- a/lib/json_schemer/draft202012/meta.rb +++ b/lib/json_schemer/draft202012/meta.rb @@ -91,9 +91,6 @@ module Meta CORE = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/core', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/core' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Core vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -141,9 +138,6 @@ module Meta APPLICATOR = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/applicator', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/applicator' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -188,9 +182,6 @@ module Meta UNEVALUATED = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/unevaluated', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/unevaluated' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Unevaluated applicator vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -202,9 +193,6 @@ module Meta VALIDATION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/validation', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/validation' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Validation vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -299,9 +287,6 @@ module Meta META_DATA = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/meta-data', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/meta-data' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Meta-data vocabulary meta-schema', 'type' => ['object', 'boolean'], @@ -334,9 +319,6 @@ module Meta FORMAT_ANNOTATION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/format-annotation', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Format vocabulary meta-schema for annotation results', 'type' => ['object', 'boolean'], @@ -347,9 +329,6 @@ module Meta FORMAT_ASSERTION = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/format-assertion', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/format-assertion' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Format vocabulary meta-schema for assertion results', 'type' => ['object', 'boolean'], @@ -360,9 +339,6 @@ module Meta CONTENT = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://json-schema.org/draft/2020-12/meta/content', - '$vocabulary' => { - 'https://json-schema.org/draft/2020-12/vocab/content' => true - }, '$dynamicAnchor' => 'meta', 'title' => 'Content vocabulary meta-schema', 'type' => ['object', 'boolean'], diff --git a/test/fixtures/draft2019-09.json b/test/fixtures/draft2019-09.json index bf9baff6..f7007ba4 100644 --- a/test/fixtures/draft2019-09.json +++ b/test/fixtures/draft2019-09.json @@ -1691,9 +1691,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -1765,9 +1762,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -1839,9 +1833,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -3459,9 +3450,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -3597,9 +3585,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -5316,9 +5301,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5392,9 +5374,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5468,9 +5447,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5544,9 +5520,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5620,9 +5593,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5696,9 +5666,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -5772,9 +5739,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ @@ -18218,9 +18182,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/validation": true - }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": [ @@ -19023,9 +18984,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2019-09/vocab/core": true - }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": [ diff --git a/test/fixtures/draft2020-12.json b/test/fixtures/draft2020-12.json index b57ca081..a21dc28a 100644 --- a/test/fixtures/draft2020-12.json +++ b/test/fixtures/draft2020-12.json @@ -1494,9 +1494,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -1573,9 +1570,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -1652,9 +1646,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -3277,9 +3268,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -3415,9 +3403,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -6353,9 +6338,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6433,9 +6415,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6513,9 +6492,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6593,9 +6569,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6673,9 +6646,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6753,9 +6723,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -6833,9 +6800,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ @@ -11697,9 +11661,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -17691,9 +17652,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true - }, "$dynamicAnchor": "meta", "title": "Validation vocabulary meta-schema", "type": [ @@ -18495,9 +18453,6 @@ "root_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true - }, "$dynamicAnchor": "meta", "title": "Core vocabulary meta-schema", "type": [ From b78828f93a715b0641dbac56f5c45ee72593f768 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Wed, 15 Nov 2023 12:24:59 -0800 Subject: [PATCH 16/18] 2.1.0 Features: - Custom error messages - Custom content encodings and media types See individual commits for more details. Closes: - https://github.com/davishmcclurg/json_schemer/issues/137 - https://github.com/davishmcclurg/json_schemer/issues/143 - https://github.com/davishmcclurg/json_schemer/issues/144 - https://github.com/davishmcclurg/json_schemer/issues/146 --- CHANGELOG.md | 13 ++++++++++--- Gemfile.lock | 6 +++--- lib/json_schemer/version.rb | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af29cf55..9c380084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,16 @@ ### Bug Fixes -- Limit anyOf/oneOf discriminator to listed refs -- Require discriminator `propertyName` property -- Support `Schema#ref` in subschemas +- Limit anyOf/oneOf discriminator to listed refs: https://github.com/davishmcclurg/json_schemer/pull/145 +- Require discriminator `propertyName` property: https://github.com/davishmcclurg/json_schemer/pull/145 +- Support `Schema#ref` in subschemas: https://github.com/davishmcclurg/json_schemer/pull/145 +- Resolve JSON pointer refs using correct base URI: https://github.com/davishmcclurg/json_schemer/pull/147 +- `date` format in OpenAPI 3.0: https://github.com/davishmcclurg/json_schemer/commit/69fe7a815ecf0cfb1c40ac402bf46a789c05e972 + +### Features + +- Custom error messages with `x-error` keyword and I18n: https://github.com/davishmcclurg/json_schemer/pull/149 +- Custom content encodings and media types: https://github.com/davishmcclurg/json_schemer/pull/148 [2.1.0]: https://github.com/davishmcclurg/json_schemer/releases/tag/v2.1.0 diff --git a/Gemfile.lock b/Gemfile.lock index 5f08ad3b..c5b256da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - json_schemer (2.0.0) + json_schemer (2.1.0) hana (~> 1.3) regexp_parser (~> 2.0) simpleidn (~> 0.2) @@ -18,7 +18,7 @@ GEM i18n (< 2) minitest (5.15.0) rake (13.0.6) - regexp_parser (2.8.1) + regexp_parser (2.8.2) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -30,7 +30,7 @@ GEM unf (0.1.4) unf_ext unf (0.1.4-java) - unf_ext (0.0.8.2) + unf_ext (0.0.9) PLATFORMS java diff --git a/lib/json_schemer/version.rb b/lib/json_schemer/version.rb index 8834a4c7..e1062643 100644 --- a/lib/json_schemer/version.rb +++ b/lib/json_schemer/version.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true module JSONSchemer - VERSION = '2.0.0' + VERSION = '2.1.0' end From 8c77a4278e0b09e2664d387267f88005497cf048 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Thu, 16 Nov 2023 12:06:59 -0800 Subject: [PATCH 17/18] Squashed 'JSON-Schema-Test-Suite/' changes from 5cc9214e..d38ddd54 d38ddd54 Merge pull request #696 from jdesrosiers/unevaluated-dynamicref 5d0c05fa Fix copy/paste error 95fe6ca2 Merge pull request #694 from json-schema-org/heterogeneous-additionalItems 9c88a0be Merge pull request #697 from json-schema-org/gregsdennis/add-ref-into-known-nonapplicator 49222046 Add unevaluted with dynamic ref tests to draft-next 8ba1c90d Update unevaluted with dynamic ref to be more likely to catch errors fea2cf19 add tests for 2019 and 2020 6695ca38 add optional tests for `$ref`ing into known non-applicator keywords 2834c630 Add tests for unevaluated with dynamic reference cda4281c Merge pull request #695 from json-schema-org/ether/clean-up-subSchemas 7b9f45c2 move subSchemas-defs.json to subSchemas.json e41ec0ec remove unused definition files 349c5a82 Merge pull request #692 from json-schema-org/ether/fix-subSchemas-refs 451baca4 Merge pull request #670 from marksparkza/invalid-output-test b8da838a Add tests for heterogeneous arrays with additionalItems 6d7a44b7 fix subschema locations and their $refs a9a1e2e3 Merge pull request #690 from skryukov/add-ipv4-mask-test ba52c48a Merge pull request #689 from skryukov/add-schema-keyword-to-required-tests 69b53add Add a test case for ipv4 with netmask d0c602a7 Add $schema keyword to required tests 20f1f52c Merge pull request #688 from spacether/feat_updates_python_exp_impl b087b3ca Updates implmentation 4ecd01f3 Merge pull request #687 from swaeberle/check-single-label-idn-hostnames 732e7275 test single label IDN hostnames ab3924a6 Merge pull request #685 from swaeberle/check-single-label-hostnames 9265a4fa do not test hostname with leading digit for older drafts 261b52db do not allow starting digits in hostnames for older drafts 9fc231ea test digits in hostnames e9b20158 test plain single label hostnames c8b57093 test valid single label hostnames 299aa7fe Merge pull request #682 from json-schema-org/useless-branch fbb3cac6 Bump the sanity check to use a released version of jsonschema ea0b63c9 Remove invalid output tests git-subtree-dir: JSON-Schema-Test-Suite git-subtree-split: d38ddd543ebc81e5c23ab03d6598c06563c38a17 --- README.md | 2 +- bin/jsonschema_suite | 9 ++- output-tests/draft2019-09/content/type.json | 26 --------- output-tests/draft2020-12/content/type.json | 26 --------- remotes/draft-next/subSchemas-defs.json | 11 ---- remotes/draft-next/subSchemas.json | 12 ++-- remotes/draft2019-09/subSchemas-defs.json | 11 ---- remotes/draft2019-09/subSchemas.json | 12 ++-- remotes/draft2020-12/subSchemas-defs.json | 11 ---- remotes/draft2020-12/subSchemas.json | 12 ++-- remotes/subSchemas-defs.json | 10 ---- remotes/subSchemas.json | 12 ++-- tests/draft-next/dependentSchemas.json | 1 + tests/draft-next/items.json | 20 +++++++ .../draft-next/optional/format/hostname.json | 25 ++++++++ .../optional/format/idn-hostname.json | 25 ++++++++ tests/draft-next/optional/format/ipv4.json | 5 ++ .../optional/refOfUnknownKeyword.json | 23 ++++++++ tests/draft-next/ref.json | 8 +++ tests/draft-next/refRemote.json | 20 +++++-- tests/draft-next/unevaluatedItems.json | 42 ++++++++++++++ tests/draft-next/unevaluatedProperties.json | 48 +++++++++++++++ tests/draft2019-09/additionalItems.json | 20 +++++++ tests/draft2019-09/dependentSchemas.json | 1 + .../optional/format/hostname.json | 25 ++++++++ .../optional/format/idn-hostname.json | 25 ++++++++ tests/draft2019-09/optional/format/ipv4.json | 5 ++ .../optional/refOfUnknownKeyword.json | 23 ++++++++ tests/draft2019-09/ref.json | 8 +++ tests/draft2019-09/refRemote.json | 20 +++++-- tests/draft2019-09/unevaluatedItems.json | 45 ++++++++++++++ tests/draft2019-09/unevaluatedProperties.json | 58 +++++++++++++++++++ tests/draft2020-12/dependentSchemas.json | 1 + tests/draft2020-12/items.json | 20 +++++++ .../optional/format/hostname.json | 25 ++++++++ .../optional/format/idn-hostname.json | 25 ++++++++ tests/draft2020-12/optional/format/ipv4.json | 5 ++ .../optional/refOfUnknownKeyword.json | 23 ++++++++ tests/draft2020-12/ref.json | 8 +++ tests/draft2020-12/refRemote.json | 20 +++++-- tests/draft2020-12/unevaluatedItems.json | 49 ++++++++++++++++ tests/draft2020-12/unevaluatedProperties.json | 55 ++++++++++++++++++ tests/draft3/additionalItems.json | 19 ++++++ tests/draft3/refRemote.json | 4 +- tests/draft4/additionalItems.json | 19 ++++++ tests/draft4/optional/format/hostname.json | 20 +++++++ tests/draft4/optional/format/ipv4.json | 5 ++ tests/draft4/refRemote.json | 4 +- tests/draft6/additionalItems.json | 19 ++++++ tests/draft6/optional/format/hostname.json | 20 +++++++ tests/draft6/optional/format/ipv4.json | 5 ++ tests/draft6/refRemote.json | 4 +- tests/draft7/additionalItems.json | 19 ++++++ tests/draft7/optional/format/hostname.json | 20 +++++++ .../draft7/optional/format/idn-hostname.json | 20 +++++++ tests/draft7/optional/format/ipv4.json | 5 ++ tests/draft7/refRemote.json | 4 +- tox.ini | 2 +- 58 files changed, 876 insertions(+), 145 deletions(-) delete mode 100644 remotes/draft-next/subSchemas-defs.json delete mode 100644 remotes/draft2019-09/subSchemas-defs.json delete mode 100644 remotes/draft2020-12/subSchemas-defs.json delete mode 100644 remotes/subSchemas-defs.json diff --git a/README.md b/README.md index cdd5dc8a..f638315c 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ Node-specific support is maintained in a [separate repository](https://github.co * [fastjsonschema](https://github.com/seznam/python-fastjsonschema) * [hypothesis-jsonschema](https://github.com/Zac-HD/hypothesis-jsonschema) * [jschon](https://github.com/marksparkza/jschon) -* [python-experimental, OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/python-experimental.md) +* [OpenAPI JSON Schema Generator](https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) ### Ruby diff --git a/bin/jsonschema_suite b/bin/jsonschema_suite index 9fee8d7b..c83e7cb2 100755 --- a/bin/jsonschema_suite +++ b/bin/jsonschema_suite @@ -14,9 +14,11 @@ import warnings try: import jsonschema.validators except ImportError: - jsonschema = None + jsonschema = Unresolvable = None VALIDATORS = {} else: + from referencing.exceptions import Unresolvable + VALIDATORS = { "draft3": jsonschema.validators.Draft3Validator, "draft4": jsonschema.validators.Draft4Validator, @@ -587,7 +589,7 @@ class SanityTests(unittest.TestCase): with self.subTest(case=case, version=version.name): try: Validator(case["schema"]).is_valid(12) - except jsonschema.exceptions.RefResolutionError: + except Unresolvable: pass @unittest.skipIf(jsonschema is None, "Validation library not present!") @@ -615,9 +617,6 @@ class SanityTests(unittest.TestCase): with self.subTest(path=path): try: validator.validate(cases) - except jsonschema.exceptions.RefResolutionError: - # python-jsonschema/jsonschema#884 - pass except jsonschema.ValidationError as error: self.fail(str(error)) diff --git a/output-tests/draft2019-09/content/type.json b/output-tests/draft2019-09/content/type.json index cff77a74..21118fd5 100644 --- a/output-tests/draft2019-09/content/type.json +++ b/output-tests/draft2019-09/content/type.json @@ -31,32 +31,6 @@ "required": ["errors"] } } - }, - { - "description": "correct type yields an output unit", - "data": "a string", - "output": { - "basic": { - "$id": "https://json-schema.org/tests/content/draft2019-09/type/0/tests/1/basic", - "$ref": "/draft/2019-09/output/schema", - "properties": { - "annotations": { - "contains": { - "properties": { - "valid": {"const": true}, - "keywordLocation": {"const": "/type"}, - "absoluteKeywordLocation": {"const": "https://json-schema.org/tests/content/draft2019-09/type/0#/type"}, - "instanceLocation": {"const": ""}, - "annotation": false, - "error": false - }, - "required": ["keywordLocation", "instanceLocation"] - } - } - }, - "required": ["annotations"] - } - } } ] } diff --git a/output-tests/draft2020-12/content/type.json b/output-tests/draft2020-12/content/type.json index 710475b2..2949a605 100644 --- a/output-tests/draft2020-12/content/type.json +++ b/output-tests/draft2020-12/content/type.json @@ -31,32 +31,6 @@ "required": ["errors"] } } - }, - { - "description": "correct type yields an output unit", - "data": "a string", - "output": { - "basic": { - "$id": "https://json-schema.org/tests/content/draft2020-12/type/0/tests/1/basic", - "$ref": "/draft/2020-12/output/schema", - "properties": { - "annotations": { - "contains": { - "properties": { - "valid": {"const": true}, - "keywordLocation": {"const": "/type"}, - "absoluteKeywordLocation": {"const": "https://json-schema.org/tests/content/draft2020-12/type/0#/type"}, - "instanceLocation": {"const": ""}, - "annotation": false, - "error": false - }, - "required": ["keywordLocation", "instanceLocation"] - } - } - }, - "required": ["annotations"] - } - } } ] } diff --git a/remotes/draft-next/subSchemas-defs.json b/remotes/draft-next/subSchemas-defs.json deleted file mode 100644 index 75b7583c..00000000 --- a/remotes/draft-next/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/remotes/draft-next/subSchemas.json b/remotes/draft-next/subSchemas.json index 575dd00c..75b7583c 100644 --- a/remotes/draft-next/subSchemas.json +++ b/remotes/draft-next/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/next/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/remotes/draft2019-09/subSchemas-defs.json b/remotes/draft2019-09/subSchemas-defs.json deleted file mode 100644 index fdfee68d..00000000 --- a/remotes/draft2019-09/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/remotes/draft2019-09/subSchemas.json b/remotes/draft2019-09/subSchemas.json index 6dea2252..fdfee68d 100644 --- a/remotes/draft2019-09/subSchemas.json +++ b/remotes/draft2019-09/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/remotes/draft2020-12/subSchemas-defs.json b/remotes/draft2020-12/subSchemas-defs.json deleted file mode 100644 index 1bb4846d..00000000 --- a/remotes/draft2020-12/subSchemas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/remotes/draft2020-12/subSchemas.json b/remotes/draft2020-12/subSchemas.json index 5fca21d8..1bb4846d 100644 --- a/remotes/draft2020-12/subSchemas.json +++ b/remotes/draft2020-12/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/remotes/subSchemas-defs.json b/remotes/subSchemas-defs.json deleted file mode 100644 index 50b7b6dc..00000000 --- a/remotes/subSchemas-defs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/remotes/subSchemas.json b/remotes/subSchemas.json index 9f8030bc..6e9b3de3 100644 --- a/remotes/subSchemas.json +++ b/remotes/subSchemas.json @@ -1,8 +1,10 @@ { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } } diff --git a/tests/draft-next/dependentSchemas.json b/tests/draft-next/dependentSchemas.json index 8a847759..86079c34 100644 --- a/tests/draft-next/dependentSchemas.json +++ b/tests/draft-next/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "properties": { "foo": {} }, diff --git a/tests/draft-next/items.json b/tests/draft-next/items.json index 459943be..dfb79af2 100644 --- a/tests/draft-next/items.json +++ b/tests/draft-next/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/tests/draft-next/optional/format/hostname.json b/tests/draft-next/optional/format/hostname.json index 96784865..bfb30636 100644 --- a/tests/draft-next/optional/format/hostname.json +++ b/tests/draft-next/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft-next/optional/format/idn-hostname.json b/tests/draft-next/optional/format/idn-hostname.json index ee2e792f..109bf73c 100644 --- a/tests/draft-next/optional/format/idn-hostname.json +++ b/tests/draft-next/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft-next/optional/format/ipv4.json b/tests/draft-next/optional/format/ipv4.json index e3e94401..2a4bc2b2 100644 --- a/tests/draft-next/optional/format/ipv4.json +++ b/tests/draft-next/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft-next/optional/refOfUnknownKeyword.json b/tests/draft-next/optional/refOfUnknownKeyword.json index 489701cd..c832e09f 100644 --- a/tests/draft-next/optional/refOfUnknownKeyword.json +++ b/tests/draft-next/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/tests/draft-next/ref.json b/tests/draft-next/ref.json index 1d5f2561..8417ce29 100644 --- a/tests/draft-next/ref.json +++ b/tests/draft-next/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$defs": { "": { "$defs": { diff --git a/tests/draft-next/refRemote.json b/tests/draft-next/refRemote.json index 9befceb2..647fb9f1 100644 --- a/tests/draft-next/refRemote.json +++ b/tests/draft-next/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://localhost:1234/draft-next/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/tests/draft-next/unevaluatedItems.json b/tests/draft-next/unevaluatedItems.json index 7379afb4..8dda001f 100644 --- a/tests/draft-next/unevaluatedItems.json +++ b/tests/draft-next/unevaluatedItems.json @@ -461,6 +461,48 @@ } ] }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/tests/draft-next/unevaluatedProperties.json b/tests/draft-next/unevaluatedProperties.json index 69fe8a00..4fe7986d 100644 --- a/tests/draft-next/unevaluatedProperties.json +++ b/tests/draft-next/unevaluatedProperties.json @@ -715,6 +715,54 @@ } ] }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/tests/draft2019-09/additionalItems.json b/tests/draft2019-09/additionalItems.json index aa44bcb7..9a7ae4f8 100644 --- a/tests/draft2019-09/additionalItems.json +++ b/tests/draft2019-09/additionalItems.json @@ -182,6 +182,26 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/tests/draft2019-09/dependentSchemas.json b/tests/draft2019-09/dependentSchemas.json index 3577efdf..c5b8ea05 100644 --- a/tests/draft2019-09/dependentSchemas.json +++ b/tests/draft2019-09/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": {} }, diff --git a/tests/draft2019-09/optional/format/hostname.json b/tests/draft2019-09/optional/format/hostname.json index eac8cac6..f3b7181c 100644 --- a/tests/draft2019-09/optional/format/hostname.json +++ b/tests/draft2019-09/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft2019-09/optional/format/idn-hostname.json b/tests/draft2019-09/optional/format/idn-hostname.json index 72f17975..072a6b08 100644 --- a/tests/draft2019-09/optional/format/idn-hostname.json +++ b/tests/draft2019-09/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft2019-09/optional/format/ipv4.json b/tests/draft2019-09/optional/format/ipv4.json index ac1e14c6..efe42471 100644 --- a/tests/draft2019-09/optional/format/ipv4.json +++ b/tests/draft2019-09/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft2019-09/optional/refOfUnknownKeyword.json b/tests/draft2019-09/optional/refOfUnknownKeyword.json index eee1c33e..e9a75dd1 100644 --- a/tests/draft2019-09/optional/refOfUnknownKeyword.json +++ b/tests/draft2019-09/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/tests/draft2019-09/ref.json b/tests/draft2019-09/ref.json index 7d850414..ea569908 100644 --- a/tests/draft2019-09/ref.json +++ b/tests/draft2019-09/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$defs": { "": { "$defs": { diff --git a/tests/draft2019-09/refRemote.json b/tests/draft2019-09/refRemote.json index 0bacbfc2..072894cf 100644 --- a/tests/draft2019-09/refRemote.json +++ b/tests/draft2019-09/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://localhost:1234/draft2019-09/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/tests/draft2019-09/unevaluatedItems.json b/tests/draft2019-09/unevaluatedItems.json index 53565a0b..9c115ab3 100644 --- a/tests/draft2019-09/unevaluatedItems.json +++ b/tests/draft2019-09/unevaluatedItems.json @@ -480,6 +480,51 @@ } ] }, + { + "description": "unevaluatedItems with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "/tree", + "items": [ + true, + true, + { "type": "string" } + ], + + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + + "type": "array", + "items": [ + { "type": "number" }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [1, [2, [], "b"], "a"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": [1, [2, [], "b", "too many"], "a"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/tests/draft2019-09/unevaluatedProperties.json b/tests/draft2019-09/unevaluatedProperties.json index a6cce8bb..4e0d3ec8 100644 --- a/tests/draft2019-09/unevaluatedProperties.json +++ b/tests/draft2019-09/unevaluatedProperties.json @@ -715,6 +715,64 @@ } ] }, + { + "description": "unevaluatedProperties with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "/tree", + "properties": { + "name": { "type": "string" } + }, + + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": ["node"] + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "name": "b", + "node": 2 + } + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "foo": "b", + "node": 2 + } + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/tests/draft2020-12/dependentSchemas.json b/tests/draft2020-12/dependentSchemas.json index 66ac0eb4..1c5f0574 100644 --- a/tests/draft2020-12/dependentSchemas.json +++ b/tests/draft2020-12/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": {} }, diff --git a/tests/draft2020-12/items.json b/tests/draft2020-12/items.json index 1ef18bdd..6a3e1cf2 100644 --- a/tests/draft2020-12/items.json +++ b/tests/draft2020-12/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/tests/draft2020-12/optional/format/hostname.json b/tests/draft2020-12/optional/format/hostname.json index c8db9770..41418dd4 100644 --- a/tests/draft2020-12/optional/format/hostname.json +++ b/tests/draft2020-12/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft2020-12/optional/format/idn-hostname.json b/tests/draft2020-12/optional/format/idn-hostname.json index 5549c055..bc7d92f6 100644 --- a/tests/draft2020-12/optional/format/idn-hostname.json +++ b/tests/draft2020-12/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft2020-12/optional/format/ipv4.json b/tests/draft2020-12/optional/format/ipv4.json index c72b6fc2..86d27bdb 100644 --- a/tests/draft2020-12/optional/format/ipv4.json +++ b/tests/draft2020-12/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft2020-12/optional/refOfUnknownKeyword.json b/tests/draft2020-12/optional/refOfUnknownKeyword.json index f91c1888..c2b080a1 100644 --- a/tests/draft2020-12/optional/refOfUnknownKeyword.json +++ b/tests/draft2020-12/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/tests/draft2020-12/ref.json b/tests/draft2020-12/ref.json index 5f6be8c2..8d15fa43 100644 --- a/tests/draft2020-12/ref.json +++ b/tests/draft2020-12/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { diff --git a/tests/draft2020-12/refRemote.json b/tests/draft2020-12/refRemote.json index ea4177f0..047ac74c 100644 --- a/tests/draft2020-12/refRemote.json +++ b/tests/draft2020-12/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -314,6 +323,7 @@ { "description": "$ref to $ref finds detached $anchor", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" }, "tests": [ diff --git a/tests/draft2020-12/unevaluatedItems.json b/tests/draft2020-12/unevaluatedItems.json index 2615c4c4..ddc35da2 100644 --- a/tests/draft2020-12/unevaluatedItems.json +++ b/tests/draft2020-12/unevaluatedItems.json @@ -461,6 +461,55 @@ } ] }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/tests/draft2020-12/unevaluatedProperties.json b/tests/draft2020-12/unevaluatedProperties.json index f7fb420f..023e84a5 100644 --- a/tests/draft2020-12/unevaluatedProperties.json +++ b/tests/draft2020-12/unevaluatedProperties.json @@ -715,6 +715,61 @@ } ] }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + + "$ref": "/baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "/baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { diff --git a/tests/draft3/additionalItems.json b/tests/draft3/additionalItems.json index 0cb66870..ab44a2eb 100644 --- a/tests/draft3/additionalItems.json +++ b/tests/draft3/additionalItems.json @@ -110,6 +110,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/tests/draft3/refRemote.json b/tests/draft3/refRemote.json index de0cb43a..0e4ab53e 100644 --- a/tests/draft3/refRemote.json +++ b/tests/draft3/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/tests/draft4/additionalItems.json b/tests/draft4/additionalItems.json index deb44fd3..c9e68154 100644 --- a/tests/draft4/additionalItems.json +++ b/tests/draft4/additionalItems.json @@ -146,6 +146,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/tests/draft4/optional/format/hostname.json b/tests/draft4/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/tests/draft4/optional/format/hostname.json +++ b/tests/draft4/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft4/optional/format/ipv4.json b/tests/draft4/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/tests/draft4/optional/format/ipv4.json +++ b/tests/draft4/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft4/refRemote.json b/tests/draft4/refRemote.json index 412c9ff8..64a618b8 100644 --- a/tests/draft4/refRemote.json +++ b/tests/draft4/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/tests/draft6/additionalItems.json b/tests/draft6/additionalItems.json index cae72361..2c7d1558 100644 --- a/tests/draft6/additionalItems.json +++ b/tests/draft6/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/tests/draft6/optional/format/hostname.json b/tests/draft6/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/tests/draft6/optional/format/hostname.json +++ b/tests/draft6/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft6/optional/format/ipv4.json b/tests/draft6/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/tests/draft6/optional/format/ipv4.json +++ b/tests/draft6/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft6/refRemote.json b/tests/draft6/refRemote.json index 5d60fae1..28459c4a 100644 --- a/tests/draft6/refRemote.json +++ b/tests/draft6/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/tests/draft7/additionalItems.json b/tests/draft7/additionalItems.json index cae72361..2c7d1558 100644 --- a/tests/draft7/additionalItems.json +++ b/tests/draft7/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/tests/draft7/optional/format/hostname.json b/tests/draft7/optional/format/hostname.json index 8a67fda8..a8ecd194 100644 --- a/tests/draft7/optional/format/hostname.json +++ b/tests/draft7/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft7/optional/format/idn-hostname.json b/tests/draft7/optional/format/idn-hostname.json index 6c8f86a3..dc47f7b5 100644 --- a/tests/draft7/optional/format/idn-hostname.json +++ b/tests/draft7/optional/format/idn-hostname.json @@ -298,6 +298,26 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/tests/draft7/optional/format/ipv4.json b/tests/draft7/optional/format/ipv4.json index 4706581f..9680fe62 100644 --- a/tests/draft7/optional/format/ipv4.json +++ b/tests/draft7/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/tests/draft7/refRemote.json b/tests/draft7/refRemote.json index 115e12e7..22185d67 100644 --- a/tests/draft7/refRemote.json +++ b/tests/draft7/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/tox.ini b/tox.ini index dcc0dce6..a5ded970 100644 --- a/tox.ini +++ b/tox.ini @@ -5,5 +5,5 @@ skipsdist = True [testenv:sanity] # used just for validating the structure of the test case files themselves -deps = jsonschema==4.18.0a4 +deps = jsonschema==4.19.0 commands = {envpython} bin/jsonschema_suite check From a47f148ce8a28932a7de98b6ffefd0c9f12d8f46 Mon Sep 17 00:00:00 2001 From: David Harsha Date: Thu, 16 Nov 2023 14:30:55 -0800 Subject: [PATCH 18/18] Update fixtures From json-schema-test-suite update. --- test/fixtures/draft2019-09.json | 206 ++++++++++++++++++++++++++++++ test/fixtures/draft2020-12.json | 214 ++++++++++++++++++++++++++++++++ test/fixtures/draft4.json | 89 +++++++++++-- test/fixtures/draft6.json | 89 +++++++++++-- test/fixtures/draft7.json | 101 +++++++++++++-- 5 files changed, 663 insertions(+), 36 deletions(-) diff --git a/test/fixtures/draft2019-09.json b/test/fixtures/draft2019-09.json index f7007ba4..da884b6e 100644 --- a/test/fixtures/draft2019-09.json +++ b/test/fixtures/draft2019-09.json @@ -190,6 +190,43 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -4210,6 +4247,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": { } @@ -4237,6 +4275,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": { } @@ -12021,6 +12060,21 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -12615,6 +12669,21 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -12755,6 +12824,22 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -14983,6 +15068,32 @@ "type": "integer" } ] + ], + [ + [ + + ], + [ + { + "data": 42, + "data_pointer": "", + "schema": { + "type": "string" + }, + "schema_pointer": "/examples/0", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/base", + "examples": [ + { + "type": "string" + } + ], + "$ref": "#/examples/0" + }, + "type": "string" + } + ] ] ], "JSON-Schema-Test-Suite/tests/draft2019-09/pattern.json": [ @@ -19119,6 +19230,7 @@ }, "schema_pointer": "/$defs/foo/$defs/bar", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -19147,6 +19259,7 @@ }, "schema_pointer": "/if", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -19171,6 +19284,7 @@ }, "schema_pointer": "/then", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -19195,6 +19309,7 @@ }, "schema_pointer": "/else", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -19222,6 +19337,7 @@ }, "schema_pointer": "/$defs/b", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -19252,6 +19368,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -19277,6 +19394,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -19302,6 +19420,7 @@ }, "schema_pointer": "/$defs//$defs/", "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$defs": { "": { "$defs": { @@ -21655,6 +21774,50 @@ } ] ], + [ + [ + + ], + [ + { + "data": "too many", + "data_pointer": "/1/3", + "schema": false, + "schema_pointer": "/$defs/tree/items/1/unevaluatedItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + "$recursiveAnchor": true, + "$ref": "/tree", + "items": [ + true, + true, + { + "type": "string" + } + ], + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + "type": "array", + "items": [ + { + "type": "number" + }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "type": "schema" + } + ] + ], [ [ { @@ -22625,6 +22788,49 @@ } ] ], + [ + [ + + ], + [ + { + "data": "b", + "data_pointer": "/branches/foo", + "schema": false, + "schema_pointer": "/$defs/tree/properties/branches/unevaluatedProperties", + "root_schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/extended-tree", + "$recursiveAnchor": true, + "$ref": "/tree", + "properties": { + "name": { + "type": "string" + } + }, + "$defs": { + "tree": { + "$id": "/tree", + "$recursiveAnchor": true, + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": [ + "node" + ] + } + } + }, + "type": "schema" + } + ] + ], [ [ { diff --git a/test/fixtures/draft2020-12.json b/test/fixtures/draft2020-12.json index a21dc28a..7ffbab6a 100644 --- a/test/fixtures/draft2020-12.json +++ b/test/fixtures/draft2020-12.json @@ -4028,6 +4028,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": { } @@ -4055,6 +4056,7 @@ "schema": false, "schema_pointer": "/dependentSchemas/foo/additionalProperties", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": { } @@ -7933,6 +7935,43 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/items", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + } + ], + "items": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/items", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + } + ], + "items": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -13249,6 +13288,21 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -13843,6 +13897,21 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -13983,6 +14052,22 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -16259,6 +16344,32 @@ "type": "integer" } ] + ], + [ + [ + + ], + [ + { + "data": 42, + "data_pointer": "", + "schema": { + "type": "string" + }, + "schema_pointer": "/examples/0", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/base", + "examples": [ + { + "type": "string" + } + ], + "$ref": "#/examples/0" + }, + "type": "string" + } + ] ] ], "JSON-Schema-Test-Suite/tests/draft2020-12/pattern.json": [ @@ -18593,6 +18704,7 @@ }, "schema_pointer": "/$defs/foo/$defs/bar", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -18621,6 +18733,7 @@ }, "schema_pointer": "/if", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -18645,6 +18758,7 @@ }, "schema_pointer": "/then", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -18669,6 +18783,7 @@ }, "schema_pointer": "/else", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -18696,6 +18811,7 @@ }, "schema_pointer": "/$defs/b", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -18726,6 +18842,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -18751,6 +18868,7 @@ }, "schema_pointer": "/$defs/foo", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -18776,6 +18894,7 @@ }, "schema_pointer": "/$defs//$defs/", "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { @@ -21136,6 +21255,54 @@ } ] ], + [ + [ + + ], + [ + { + "data": "baz", + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/$defs/baseSchema/unevaluatedItems", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + "$ref": "/baseSchema", + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { + "type": "string" + } + ] + }, + "baseSchema": { + "$id": "/baseSchema", + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "$dynamicRef": "#addons", + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "type": "schema" + } + ] + ], [ [ { @@ -22594,6 +22761,53 @@ } ] ], + [ + [ + + ], + [ + { + "data": "baz", + "data_pointer": "/baz", + "schema": false, + "schema_pointer": "/$defs/baseSchema/unevaluatedProperties", + "root_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/derived", + "$ref": "/baseSchema", + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { + "type": "string" + } + } + }, + "baseSchema": { + "$id": "/baseSchema", + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "$dynamicRef": "#addons", + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "type": "schema" + } + ] + ], [ [ { diff --git a/test/fixtures/draft4.json b/test/fixtures/draft4.json index 4eb61ce1..9850ed7e 100644 --- a/test/fixtures/draft4.json +++ b/test/fixtures/draft4.json @@ -143,6 +143,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -6451,6 +6486,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -6577,6 +6624,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -9020,13 +9081,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -9044,13 +9107,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" diff --git a/test/fixtures/draft6.json b/test/fixtures/draft6.json index 1ad115bf..4ae2b503 100644 --- a/test/fixtures/draft6.json +++ b/test/fixtures/draft6.json @@ -184,6 +184,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -8117,6 +8152,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -8243,6 +8290,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -11664,13 +11725,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -11688,13 +11751,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" diff --git a/test/fixtures/draft7.json b/test/fixtures/draft7.json index 522d49af..6c8092e8 100644 --- a/test/fixtures/draft7.json +++ b/test/fixtures/draft7.json @@ -184,6 +184,41 @@ } ] ], + [ + [ + { + "data": "bar", + "data_pointer": "/1", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + }, + { + "data": 37, + "data_pointer": "/2", + "schema": false, + "schema_pointer": "/additionalItems", + "root_schema": { + "items": [ + { + } + ], + "additionalItems": false + }, + "type": "schema" + } + ], + [ + + ] + ], [ [ @@ -9052,6 +9087,18 @@ }, "type": "format" } + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -9584,6 +9631,18 @@ ], [ + ], + [ + + ], + [ + + ], + [ + + ], + [ + ] ] ], @@ -9710,6 +9769,20 @@ }, "type": "format" } + ], + [ + { + "data": "192.168.1.0/24", + "data_pointer": "", + "schema": { + "format": "ipv4" + }, + "schema_pointer": "", + "root_schema": { + "format": "ipv4" + }, + "type": "format" + } ] ] ], @@ -14049,13 +14122,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer" @@ -14073,13 +14148,15 @@ "schema": { "type": "integer" }, - "schema_pointer": "/integer", + "schema_pointer": "/definitions/integer", "root_schema": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } }, "type": "integer"