diff options
-rw-r--r-- | .coveragerc | 10 | ||||
-rw-r--r-- | .github/FUNDING.yml | 4 | ||||
-rw-r--r-- | .github/SECURITY.md | 21 | ||||
-rw-r--r-- | .github/coverage.sh | 3 | ||||
-rw-r--r-- | .github/release.yml | 5 | ||||
-rw-r--r-- | .github/workflows/ci.yml | 201 | ||||
-rw-r--r-- | .github/workflows/documentation-links.yml | 16 | ||||
-rw-r--r-- | .github/workflows/fuzz.yml | 30 | ||||
-rw-r--r-- | .gitignore | 17 | ||||
-rw-r--r-- | .pre-commit-config.yaml | 21 | ||||
-rw-r--r-- | .pre-commit-hooks.yaml | 6 | ||||
-rw-r--r-- | .readthedocs.yaml | 19 | ||||
-rw-r--r-- | CHANGELOG.rst | 517 | ||||
-rw-r--r-- | CONTRIBUTING.rst | 60 | ||||
-rw-r--r-- | COPYING | 19 | ||||
-rw-r--r-- | README.rst | 141 | ||||
-rw-r--r-- | codecov.yml | 5 | ||||
-rw-r--r-- | docs/Makefile | 227 | ||||
-rw-r--r-- | docs/api/index.rst | 24 | ||||
-rw-r--r-- | docs/api/jsonschema/exceptions/index.rst | 6 | ||||
-rw-r--r-- | docs/api/jsonschema/protocols/index.rst | 6 | ||||
-rw-r--r-- | docs/api/jsonschema/validators/index.rst | 7 | ||||
-rw-r--r-- | docs/conf.py | 136 | ||||
-rw-r--r-- | docs/creating.rst | 38 | ||||
-rw-r--r-- | docs/errors.rst | 412 | ||||
-rw-r--r-- | docs/faq.rst | 263 | ||||
-rw-r--r-- | docs/index.rst | 24 | ||||
-rw-r--r-- | docs/make.bat | 190 | ||||
-rw-r--r-- | docs/referencing.rst | 375 | ||||
-rw-r--r-- | docs/requirements.in | 10 | ||||
-rw-r--r-- | docs/requirements.txt | 137 | ||||
-rw-r--r-- | docs/spelling-wordlist.txt | 59 | ||||
-rw-r--r-- | docs/validate.rst | 306 | ||||
-rw-r--r-- | json/.editorconfig (renamed from .editorconfig) | 0 | ||||
-rw-r--r-- | json/.github/CODEOWNERS (renamed from .github/CODEOWNERS) | 0 | ||||
-rw-r--r-- | json/.github/workflows/ci.yml | 25 | ||||
-rw-r--r-- | json/.gitignore | 160 | ||||
-rw-r--r-- | json/CONTRIBUTING.md (renamed from CONTRIBUTING.md) | 0 | ||||
-rw-r--r-- | json/LICENSE (renamed from LICENSE) | 0 | ||||
-rw-r--r-- | json/README.md (renamed from README.md) | 0 | ||||
-rwxr-xr-x | json/bin/jsonschema_suite (renamed from bin/jsonschema_suite) | 0 | ||||
-rw-r--r-- | json/output-test-schema.json (renamed from output-test-schema.json) | 0 | ||||
-rw-r--r-- | json/output-tests/README.md (renamed from output-tests/README.md) | 0 | ||||
-rw-r--r-- | json/output-tests/draft-next/content/general.json (renamed from output-tests/draft-next/content/general.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft-next/content/readOnly.json (renamed from output-tests/draft-next/content/readOnly.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft-next/content/type.json (renamed from output-tests/draft-next/content/type.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft-next/output-schema.json (renamed from output-tests/draft-next/output-schema.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2019-09/content/general.json (renamed from output-tests/draft2019-09/content/general.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2019-09/content/readOnly.json (renamed from output-tests/draft2019-09/content/readOnly.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2019-09/content/type.json (renamed from output-tests/draft2019-09/content/type.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2019-09/output-schema.json (renamed from output-tests/draft2019-09/output-schema.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2020-12/content/general.json (renamed from output-tests/draft2020-12/content/general.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2020-12/content/readOnly.json (renamed from output-tests/draft2020-12/content/readOnly.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2020-12/content/type.json (renamed from output-tests/draft2020-12/content/type.json) | 0 | ||||
-rw-r--r-- | json/output-tests/draft2020-12/output-schema.json (renamed from output-tests/draft2020-12/output-schema.json) | 0 | ||||
-rw-r--r-- | json/package.json (renamed from package.json) | 0 | ||||
-rw-r--r-- | json/remotes/baseUriChange/folderInteger.json (renamed from remotes/baseUriChange/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/baseUriChangeFolder/folderInteger.json (renamed from remotes/baseUriChangeFolder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/baseUriChangeFolderInSubschema/folderInteger.json (renamed from remotes/baseUriChangeFolderInSubschema/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/different-id-ref-string.json (renamed from remotes/different-id-ref-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/baseUriChange/folderInteger.json (renamed from remotes/draft-next/baseUriChange/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/baseUriChangeFolder/folderInteger.json (renamed from remotes/draft-next/baseUriChangeFolder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json (renamed from remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/extendible-dynamic-ref.json (renamed from remotes/draft-next/extendible-dynamic-ref.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/format-assertion-false.json (renamed from remotes/draft-next/format-assertion-false.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/format-assertion-true.json (renamed from remotes/draft-next/format-assertion-true.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/integer.json (renamed from remotes/draft-next/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/locationIndependentIdentifier.json (renamed from remotes/draft-next/locationIndependentIdentifier.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/metaschema-no-validation.json (renamed from remotes/draft-next/metaschema-no-validation.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/metaschema-optional-vocabulary.json (renamed from remotes/draft-next/metaschema-optional-vocabulary.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/name-defs.json (renamed from remotes/draft-next/name-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/nested/foo-ref-string.json (renamed from remotes/draft-next/nested/foo-ref-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/nested/string.json (renamed from remotes/draft-next/nested/string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/ref-and-defs.json (renamed from remotes/draft-next/ref-and-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/subSchemas-defs.json (renamed from remotes/draft-next/subSchemas-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/subSchemas.json (renamed from remotes/draft-next/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft-next/tree.json (renamed from remotes/draft-next/tree.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/baseUriChange/folderInteger.json (renamed from remotes/draft2019-09/baseUriChange/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/baseUriChangeFolder/folderInteger.json (renamed from remotes/draft2019-09/baseUriChangeFolder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json (renamed from remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/dependentRequired.json (renamed from remotes/draft2019-09/dependentRequired.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/extendible-dynamic-ref.json (renamed from remotes/draft2019-09/extendible-dynamic-ref.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/ignore-prefixItems.json (renamed from remotes/draft2019-09/ignore-prefixItems.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/integer.json (renamed from remotes/draft2019-09/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/locationIndependentIdentifier.json (renamed from remotes/draft2019-09/locationIndependentIdentifier.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/metaschema-no-validation.json (renamed from remotes/draft2019-09/metaschema-no-validation.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/metaschema-optional-vocabulary.json (renamed from remotes/draft2019-09/metaschema-optional-vocabulary.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/name-defs.json (renamed from remotes/draft2019-09/name-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/nested/foo-ref-string.json (renamed from remotes/draft2019-09/nested/foo-ref-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/nested/string.json (renamed from remotes/draft2019-09/nested/string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/ref-and-defs.json (renamed from remotes/draft2019-09/ref-and-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/subSchemas-defs.json (renamed from remotes/draft2019-09/subSchemas-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/subSchemas.json (renamed from remotes/draft2019-09/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2019-09/tree.json (renamed from remotes/draft2019-09/tree.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/baseUriChange/folderInteger.json (renamed from remotes/draft2020-12/baseUriChange/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json (renamed from remotes/draft2020-12/baseUriChangeFolder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json (renamed from remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/extendible-dynamic-ref.json (renamed from remotes/draft2020-12/extendible-dynamic-ref.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/format-assertion-false.json (renamed from remotes/draft2020-12/format-assertion-false.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/format-assertion-true.json (renamed from remotes/draft2020-12/format-assertion-true.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/integer.json (renamed from remotes/draft2020-12/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/locationIndependentIdentifier.json (renamed from remotes/draft2020-12/locationIndependentIdentifier.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/metaschema-no-validation.json (renamed from remotes/draft2020-12/metaschema-no-validation.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/metaschema-optional-vocabulary.json (renamed from remotes/draft2020-12/metaschema-optional-vocabulary.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/name-defs.json (renamed from remotes/draft2020-12/name-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/nested/foo-ref-string.json (renamed from remotes/draft2020-12/nested/foo-ref-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/nested/string.json (renamed from remotes/draft2020-12/nested/string.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/prefixItems.json (renamed from remotes/draft2020-12/prefixItems.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/ref-and-defs.json (renamed from remotes/draft2020-12/ref-and-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/subSchemas-defs.json (renamed from remotes/draft2020-12/subSchemas-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/subSchemas.json (renamed from remotes/draft2020-12/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft2020-12/tree.json (renamed from remotes/draft2020-12/tree.json) | 0 | ||||
-rw-r--r-- | json/remotes/draft7/ignore-dependentRequired.json (renamed from remotes/draft7/ignore-dependentRequired.json) | 0 | ||||
-rw-r--r-- | json/remotes/extendible-dynamic-ref.json (renamed from remotes/extendible-dynamic-ref.json) | 0 | ||||
-rw-r--r-- | json/remotes/integer.json (renamed from remotes/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/locationIndependentIdentifier.json (renamed from remotes/locationIndependentIdentifier.json) | 0 | ||||
-rw-r--r-- | json/remotes/locationIndependentIdentifierDraft4.json (renamed from remotes/locationIndependentIdentifierDraft4.json) | 0 | ||||
-rw-r--r-- | json/remotes/locationIndependentIdentifierPre2019.json (renamed from remotes/locationIndependentIdentifierPre2019.json) | 0 | ||||
-rw-r--r-- | json/remotes/name-defs.json (renamed from remotes/name-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/name.json (renamed from remotes/name.json) | 0 | ||||
-rw-r--r-- | json/remotes/nested-absolute-ref-to-string.json (renamed from remotes/nested-absolute-ref-to-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/nested/foo-ref-string.json (renamed from remotes/nested/foo-ref-string.json) | 0 | ||||
-rw-r--r-- | json/remotes/nested/string.json (renamed from remotes/nested/string.json) | 0 | ||||
-rw-r--r-- | json/remotes/ref-and-definitions.json (renamed from remotes/ref-and-definitions.json) | 0 | ||||
-rw-r--r-- | json/remotes/ref-and-defs.json (renamed from remotes/ref-and-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/subSchemas-defs.json (renamed from remotes/subSchemas-defs.json) | 0 | ||||
-rw-r--r-- | json/remotes/subSchemas.json (renamed from remotes/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/remotes/tree.json (renamed from remotes/tree.json) | 0 | ||||
-rw-r--r-- | json/remotes/urn-ref-string.json (renamed from remotes/urn-ref-string.json) | 0 | ||||
-rw-r--r-- | json/test-schema.json (renamed from test-schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/additionalProperties.json (renamed from tests/draft-next/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/allOf.json (renamed from tests/draft-next/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/anchor.json (renamed from tests/draft-next/anchor.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/anyOf.json (renamed from tests/draft-next/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/boolean_schema.json (renamed from tests/draft-next/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/const.json (renamed from tests/draft-next/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/contains.json (renamed from tests/draft-next/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/content.json (renamed from tests/draft-next/content.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/default.json (renamed from tests/draft-next/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/defs.json (renamed from tests/draft-next/defs.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/dependentRequired.json (renamed from tests/draft-next/dependentRequired.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/dependentSchemas.json (renamed from tests/draft-next/dependentSchemas.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/dynamicRef.json (renamed from tests/draft-next/dynamicRef.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/enum.json (renamed from tests/draft-next/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/exclusiveMaximum.json (renamed from tests/draft-next/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/exclusiveMinimum.json (renamed from tests/draft-next/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/format.json (renamed from tests/draft-next/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/id.json (renamed from tests/draft-next/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/if-then-else.json (renamed from tests/draft-next/if-then-else.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/infinite-loop-detection.json (renamed from tests/draft-next/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/items.json (renamed from tests/draft-next/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/maxContains.json (renamed from tests/draft-next/maxContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/maxItems.json (renamed from tests/draft-next/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/maxLength.json (renamed from tests/draft-next/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/maxProperties.json (renamed from tests/draft-next/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/maximum.json (renamed from tests/draft-next/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/minContains.json (renamed from tests/draft-next/minContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/minItems.json (renamed from tests/draft-next/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/minLength.json (renamed from tests/draft-next/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/minProperties.json (renamed from tests/draft-next/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/minimum.json (renamed from tests/draft-next/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/multipleOf.json (renamed from tests/draft-next/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/not.json (renamed from tests/draft-next/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/oneOf.json (renamed from tests/draft-next/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/bignum.json (renamed from tests/draft-next/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/dependencies-compatibility.json (renamed from tests/draft-next/optional/dependencies-compatibility.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/ecmascript-regex.json (renamed from tests/draft-next/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/float-overflow.json (renamed from tests/draft-next/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format-assertion.json (renamed from tests/draft-next/optional/format-assertion.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/date-time.json (renamed from tests/draft-next/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/date.json (renamed from tests/draft-next/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/duration.json (renamed from tests/draft-next/optional/format/duration.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/email.json (renamed from tests/draft-next/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/hostname.json (renamed from tests/draft-next/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/idn-email.json (renamed from tests/draft-next/optional/format/idn-email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/idn-hostname.json (renamed from tests/draft-next/optional/format/idn-hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/ipv4.json (renamed from tests/draft-next/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/ipv6.json (renamed from tests/draft-next/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/iri-reference.json (renamed from tests/draft-next/optional/format/iri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/iri.json (renamed from tests/draft-next/optional/format/iri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/json-pointer.json (renamed from tests/draft-next/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/regex.json (renamed from tests/draft-next/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/relative-json-pointer.json (renamed from tests/draft-next/optional/format/relative-json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/time.json (renamed from tests/draft-next/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/uri-reference.json (renamed from tests/draft-next/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/uri-template.json (renamed from tests/draft-next/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/uri.json (renamed from tests/draft-next/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/format/uuid.json (renamed from tests/draft-next/optional/format/uuid.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/non-bmp-regex.json (renamed from tests/draft-next/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/optional/refOfUnknownKeyword.json (renamed from tests/draft-next/optional/refOfUnknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/pattern.json (renamed from tests/draft-next/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/patternProperties.json (renamed from tests/draft-next/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/prefixItems.json (renamed from tests/draft-next/prefixItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/properties.json (renamed from tests/draft-next/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/propertyDependencies.json (renamed from tests/draft-next/propertyDependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/propertyNames.json (renamed from tests/draft-next/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/ref.json (renamed from tests/draft-next/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/refRemote.json (renamed from tests/draft-next/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/required.json (renamed from tests/draft-next/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/type.json (renamed from tests/draft-next/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/unevaluatedItems.json (renamed from tests/draft-next/unevaluatedItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/unevaluatedProperties.json (renamed from tests/draft-next/unevaluatedProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/uniqueItems.json (renamed from tests/draft-next/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/unknownKeyword.json (renamed from tests/draft-next/unknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft-next/vocabulary.json (renamed from tests/draft-next/vocabulary.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/additionalItems.json (renamed from tests/draft2019-09/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/additionalProperties.json (renamed from tests/draft2019-09/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/allOf.json (renamed from tests/draft2019-09/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/anchor.json (renamed from tests/draft2019-09/anchor.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/anyOf.json (renamed from tests/draft2019-09/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/boolean_schema.json (renamed from tests/draft2019-09/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/const.json (renamed from tests/draft2019-09/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/contains.json (renamed from tests/draft2019-09/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/content.json (renamed from tests/draft2019-09/content.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/default.json (renamed from tests/draft2019-09/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/defs.json (renamed from tests/draft2019-09/defs.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/dependentRequired.json (renamed from tests/draft2019-09/dependentRequired.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/dependentSchemas.json (renamed from tests/draft2019-09/dependentSchemas.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/enum.json (renamed from tests/draft2019-09/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/exclusiveMaximum.json (renamed from tests/draft2019-09/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/exclusiveMinimum.json (renamed from tests/draft2019-09/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/format.json (renamed from tests/draft2019-09/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/id.json (renamed from tests/draft2019-09/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/if-then-else.json (renamed from tests/draft2019-09/if-then-else.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/infinite-loop-detection.json (renamed from tests/draft2019-09/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/items.json (renamed from tests/draft2019-09/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/maxContains.json (renamed from tests/draft2019-09/maxContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/maxItems.json (renamed from tests/draft2019-09/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/maxLength.json (renamed from tests/draft2019-09/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/maxProperties.json (renamed from tests/draft2019-09/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/maximum.json (renamed from tests/draft2019-09/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/minContains.json (renamed from tests/draft2019-09/minContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/minItems.json (renamed from tests/draft2019-09/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/minLength.json (renamed from tests/draft2019-09/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/minProperties.json (renamed from tests/draft2019-09/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/minimum.json (renamed from tests/draft2019-09/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/multipleOf.json (renamed from tests/draft2019-09/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/not.json (renamed from tests/draft2019-09/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/oneOf.json (renamed from tests/draft2019-09/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/bignum.json (renamed from tests/draft2019-09/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/cross-draft.json (renamed from tests/draft2019-09/optional/cross-draft.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/dependencies-compatibility.json (renamed from tests/draft2019-09/optional/dependencies-compatibility.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/ecmascript-regex.json (renamed from tests/draft2019-09/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/float-overflow.json (renamed from tests/draft2019-09/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/date-time.json (renamed from tests/draft2019-09/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/date.json (renamed from tests/draft2019-09/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/duration.json (renamed from tests/draft2019-09/optional/format/duration.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/email.json (renamed from tests/draft2019-09/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/hostname.json (renamed from tests/draft2019-09/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/idn-email.json (renamed from tests/draft2019-09/optional/format/idn-email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/idn-hostname.json (renamed from tests/draft2019-09/optional/format/idn-hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/ipv4.json (renamed from tests/draft2019-09/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/ipv6.json (renamed from tests/draft2019-09/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/iri-reference.json (renamed from tests/draft2019-09/optional/format/iri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/iri.json (renamed from tests/draft2019-09/optional/format/iri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/json-pointer.json (renamed from tests/draft2019-09/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/regex.json (renamed from tests/draft2019-09/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/relative-json-pointer.json (renamed from tests/draft2019-09/optional/format/relative-json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/time.json (renamed from tests/draft2019-09/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/unknown.json (renamed from tests/draft2019-09/optional/format/unknown.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/uri-reference.json (renamed from tests/draft2019-09/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/uri-template.json (renamed from tests/draft2019-09/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/uri.json (renamed from tests/draft2019-09/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/format/uuid.json (renamed from tests/draft2019-09/optional/format/uuid.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/no-schema.json (renamed from tests/draft2019-09/optional/no-schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/non-bmp-regex.json (renamed from tests/draft2019-09/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/optional/refOfUnknownKeyword.json (renamed from tests/draft2019-09/optional/refOfUnknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/pattern.json (renamed from tests/draft2019-09/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/patternProperties.json (renamed from tests/draft2019-09/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/properties.json (renamed from tests/draft2019-09/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/propertyNames.json (renamed from tests/draft2019-09/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/recursiveRef.json (renamed from tests/draft2019-09/recursiveRef.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/ref.json (renamed from tests/draft2019-09/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/refRemote.json (renamed from tests/draft2019-09/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/required.json (renamed from tests/draft2019-09/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/type.json (renamed from tests/draft2019-09/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/unevaluatedItems.json (renamed from tests/draft2019-09/unevaluatedItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/unevaluatedProperties.json (renamed from tests/draft2019-09/unevaluatedProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/uniqueItems.json (renamed from tests/draft2019-09/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/unknownKeyword.json (renamed from tests/draft2019-09/unknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2019-09/vocabulary.json (renamed from tests/draft2019-09/vocabulary.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/additionalProperties.json (renamed from tests/draft2020-12/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/allOf.json (renamed from tests/draft2020-12/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/anchor.json (renamed from tests/draft2020-12/anchor.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/anyOf.json (renamed from tests/draft2020-12/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/boolean_schema.json (renamed from tests/draft2020-12/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/const.json (renamed from tests/draft2020-12/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/contains.json (renamed from tests/draft2020-12/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/content.json (renamed from tests/draft2020-12/content.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/default.json (renamed from tests/draft2020-12/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/defs.json (renamed from tests/draft2020-12/defs.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/dependentRequired.json (renamed from tests/draft2020-12/dependentRequired.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/dependentSchemas.json (renamed from tests/draft2020-12/dependentSchemas.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/dynamicRef.json (renamed from tests/draft2020-12/dynamicRef.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/enum.json (renamed from tests/draft2020-12/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/exclusiveMaximum.json (renamed from tests/draft2020-12/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/exclusiveMinimum.json (renamed from tests/draft2020-12/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/format.json (renamed from tests/draft2020-12/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/id.json (renamed from tests/draft2020-12/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/if-then-else.json (renamed from tests/draft2020-12/if-then-else.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/infinite-loop-detection.json (renamed from tests/draft2020-12/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/items.json (renamed from tests/draft2020-12/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/maxContains.json (renamed from tests/draft2020-12/maxContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/maxItems.json (renamed from tests/draft2020-12/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/maxLength.json (renamed from tests/draft2020-12/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/maxProperties.json (renamed from tests/draft2020-12/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/maximum.json (renamed from tests/draft2020-12/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/minContains.json (renamed from tests/draft2020-12/minContains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/minItems.json (renamed from tests/draft2020-12/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/minLength.json (renamed from tests/draft2020-12/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/minProperties.json (renamed from tests/draft2020-12/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/minimum.json (renamed from tests/draft2020-12/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/multipleOf.json (renamed from tests/draft2020-12/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/not.json (renamed from tests/draft2020-12/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/oneOf.json (renamed from tests/draft2020-12/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/bignum.json (renamed from tests/draft2020-12/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/cross-draft.json (renamed from tests/draft2020-12/optional/cross-draft.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/dependencies-compatibility.json (renamed from tests/draft2020-12/optional/dependencies-compatibility.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/ecmascript-regex.json (renamed from tests/draft2020-12/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/float-overflow.json (renamed from tests/draft2020-12/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format-assertion.json (renamed from tests/draft2020-12/optional/format-assertion.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/date-time.json (renamed from tests/draft2020-12/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/date.json (renamed from tests/draft2020-12/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/duration.json (renamed from tests/draft2020-12/optional/format/duration.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/email.json (renamed from tests/draft2020-12/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/hostname.json (renamed from tests/draft2020-12/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/idn-email.json (renamed from tests/draft2020-12/optional/format/idn-email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/idn-hostname.json (renamed from tests/draft2020-12/optional/format/idn-hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/ipv4.json (renamed from tests/draft2020-12/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/ipv6.json (renamed from tests/draft2020-12/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/iri-reference.json (renamed from tests/draft2020-12/optional/format/iri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/iri.json (renamed from tests/draft2020-12/optional/format/iri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/json-pointer.json (renamed from tests/draft2020-12/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/regex.json (renamed from tests/draft2020-12/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/relative-json-pointer.json (renamed from tests/draft2020-12/optional/format/relative-json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/time.json (renamed from tests/draft2020-12/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/unknown.json (renamed from tests/draft2020-12/optional/format/unknown.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/uri-reference.json (renamed from tests/draft2020-12/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/uri-template.json (renamed from tests/draft2020-12/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/uri.json (renamed from tests/draft2020-12/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/format/uuid.json (renamed from tests/draft2020-12/optional/format/uuid.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/no-schema.json (renamed from tests/draft2020-12/optional/no-schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/non-bmp-regex.json (renamed from tests/draft2020-12/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/optional/refOfUnknownKeyword.json (renamed from tests/draft2020-12/optional/refOfUnknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/pattern.json (renamed from tests/draft2020-12/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/patternProperties.json (renamed from tests/draft2020-12/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/prefixItems.json (renamed from tests/draft2020-12/prefixItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/properties.json (renamed from tests/draft2020-12/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/propertyNames.json (renamed from tests/draft2020-12/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/ref.json (renamed from tests/draft2020-12/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/refRemote.json (renamed from tests/draft2020-12/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/required.json (renamed from tests/draft2020-12/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/type.json (renamed from tests/draft2020-12/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/unevaluatedItems.json (renamed from tests/draft2020-12/unevaluatedItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/unevaluatedProperties.json (renamed from tests/draft2020-12/unevaluatedProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/uniqueItems.json (renamed from tests/draft2020-12/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/unknownKeyword.json (renamed from tests/draft2020-12/unknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft2020-12/vocabulary.json (renamed from tests/draft2020-12/vocabulary.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalItems.json (renamed from tests/draft3/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalProperties.json (renamed from tests/draft3/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/default.json (renamed from tests/draft3/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/dependencies.json (renamed from tests/draft3/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/disallow.json (renamed from tests/draft3/disallow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/divisibleBy.json (renamed from tests/draft3/divisibleBy.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/enum.json (renamed from tests/draft3/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/extends.json (renamed from tests/draft3/extends.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/format.json (renamed from tests/draft3/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/infinite-loop-detection.json (renamed from tests/draft3/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/items.json (renamed from tests/draft3/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxItems.json (renamed from tests/draft3/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxLength.json (renamed from tests/draft3/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maximum.json (renamed from tests/draft3/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minItems.json (renamed from tests/draft3/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minLength.json (renamed from tests/draft3/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minimum.json (renamed from tests/draft3/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/bignum.json (renamed from tests/draft3/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/ecmascript-regex.json (renamed from tests/draft3/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/color.json (renamed from tests/draft3/optional/format/color.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/date-time.json (renamed from tests/draft3/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/date.json (renamed from tests/draft3/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/email.json (renamed from tests/draft3/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/host-name.json (renamed from tests/draft3/optional/format/host-name.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/ip-address.json (renamed from tests/draft3/optional/format/ip-address.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/ipv6.json (renamed from tests/draft3/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/regex.json (renamed from tests/draft3/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/time.json (renamed from tests/draft3/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format/uri.json (renamed from tests/draft3/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/non-bmp-regex.json (renamed from tests/draft3/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/zeroTerminatedFloats.json (renamed from tests/draft3/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/pattern.json (renamed from tests/draft3/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/patternProperties.json (renamed from tests/draft3/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/properties.json (renamed from tests/draft3/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/ref.json (renamed from tests/draft3/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/refRemote.json (renamed from tests/draft3/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/required.json (renamed from tests/draft3/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/type.json (renamed from tests/draft3/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/uniqueItems.json (renamed from tests/draft3/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalItems.json (renamed from tests/draft4/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalProperties.json (renamed from tests/draft4/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/allOf.json (renamed from tests/draft4/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/anyOf.json (renamed from tests/draft4/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/default.json (renamed from tests/draft4/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/definitions.json (renamed from tests/draft4/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/dependencies.json (renamed from tests/draft4/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/enum.json (renamed from tests/draft4/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/format.json (renamed from tests/draft4/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/id.json (renamed from tests/draft4/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/infinite-loop-detection.json (renamed from tests/draft4/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/items.json (renamed from tests/draft4/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxItems.json (renamed from tests/draft4/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxLength.json (renamed from tests/draft4/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxProperties.json (renamed from tests/draft4/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maximum.json (renamed from tests/draft4/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minItems.json (renamed from tests/draft4/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minLength.json (renamed from tests/draft4/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minProperties.json (renamed from tests/draft4/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minimum.json (renamed from tests/draft4/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/multipleOf.json (renamed from tests/draft4/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/not.json (renamed from tests/draft4/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/oneOf.json (renamed from tests/draft4/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/bignum.json (renamed from tests/draft4/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/ecmascript-regex.json (renamed from tests/draft4/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/float-overflow.json (renamed from tests/draft4/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/date-time.json (renamed from tests/draft4/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/email.json (renamed from tests/draft4/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/hostname.json (renamed from tests/draft4/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/ipv4.json (renamed from tests/draft4/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/ipv6.json (renamed from tests/draft4/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/unknown.json (renamed from tests/draft4/optional/format/unknown.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format/uri.json (renamed from tests/draft4/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/non-bmp-regex.json (renamed from tests/draft4/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/zeroTerminatedFloats.json (renamed from tests/draft4/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/pattern.json (renamed from tests/draft4/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/patternProperties.json (renamed from tests/draft4/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/properties.json (renamed from tests/draft4/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/ref.json (renamed from tests/draft4/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/refRemote.json (renamed from tests/draft4/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/required.json (renamed from tests/draft4/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/type.json (renamed from tests/draft4/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/uniqueItems.json (renamed from tests/draft4/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/additionalItems.json (renamed from tests/draft6/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/additionalProperties.json (renamed from tests/draft6/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/allOf.json (renamed from tests/draft6/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/anyOf.json (renamed from tests/draft6/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/boolean_schema.json (renamed from tests/draft6/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/const.json (renamed from tests/draft6/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/contains.json (renamed from tests/draft6/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/default.json (renamed from tests/draft6/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/definitions.json (renamed from tests/draft6/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/dependencies.json (renamed from tests/draft6/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/enum.json (renamed from tests/draft6/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/exclusiveMaximum.json (renamed from tests/draft6/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/exclusiveMinimum.json (renamed from tests/draft6/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/format.json (renamed from tests/draft6/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/id.json (renamed from tests/draft6/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/infinite-loop-detection.json (renamed from tests/draft6/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/items.json (renamed from tests/draft6/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxItems.json (renamed from tests/draft6/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxLength.json (renamed from tests/draft6/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxProperties.json (renamed from tests/draft6/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maximum.json (renamed from tests/draft6/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minItems.json (renamed from tests/draft6/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minLength.json (renamed from tests/draft6/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minProperties.json (renamed from tests/draft6/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minimum.json (renamed from tests/draft6/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/multipleOf.json (renamed from tests/draft6/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/not.json (renamed from tests/draft6/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/oneOf.json (renamed from tests/draft6/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/bignum.json (renamed from tests/draft6/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/ecmascript-regex.json (renamed from tests/draft6/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/float-overflow.json (renamed from tests/draft6/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/date-time.json (renamed from tests/draft6/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/email.json (renamed from tests/draft6/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/hostname.json (renamed from tests/draft6/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/ipv4.json (renamed from tests/draft6/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/ipv6.json (renamed from tests/draft6/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/json-pointer.json (renamed from tests/draft6/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/unknown.json (renamed from tests/draft6/optional/format/unknown.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/uri-reference.json (renamed from tests/draft6/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/uri-template.json (renamed from tests/draft6/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format/uri.json (renamed from tests/draft6/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/non-bmp-regex.json (renamed from tests/draft6/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/pattern.json (renamed from tests/draft6/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/patternProperties.json (renamed from tests/draft6/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/properties.json (renamed from tests/draft6/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/propertyNames.json (renamed from tests/draft6/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/ref.json (renamed from tests/draft6/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/refRemote.json (renamed from tests/draft6/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/required.json (renamed from tests/draft6/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/type.json (renamed from tests/draft6/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/uniqueItems.json (renamed from tests/draft6/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/unknownKeyword.json (renamed from tests/draft6/unknownKeyword.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/additionalItems.json (renamed from tests/draft7/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/additionalProperties.json (renamed from tests/draft7/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/allOf.json (renamed from tests/draft7/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/anyOf.json (renamed from tests/draft7/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/boolean_schema.json (renamed from tests/draft7/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/const.json (renamed from tests/draft7/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/contains.json (renamed from tests/draft7/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/default.json (renamed from tests/draft7/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/definitions.json (renamed from tests/draft7/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/dependencies.json (renamed from tests/draft7/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/enum.json (renamed from tests/draft7/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/exclusiveMaximum.json (renamed from tests/draft7/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/exclusiveMinimum.json (renamed from tests/draft7/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/format.json (renamed from tests/draft7/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/id.json (renamed from tests/draft7/id.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/if-then-else.json (renamed from tests/draft7/if-then-else.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/infinite-loop-detection.json (renamed from tests/draft7/infinite-loop-detection.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/items.json (renamed from tests/draft7/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxItems.json (renamed from tests/draft7/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxLength.json (renamed from tests/draft7/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxProperties.json (renamed from tests/draft7/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maximum.json (renamed from tests/draft7/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minItems.json (renamed from tests/draft7/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minLength.json (renamed from tests/draft7/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minProperties.json (renamed from tests/draft7/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minimum.json (renamed from tests/draft7/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/multipleOf.json (renamed from tests/draft7/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/not.json (renamed from tests/draft7/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/oneOf.json (renamed from tests/draft7/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/bignum.json (renamed from tests/draft7/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/content.json (renamed from tests/draft7/optional/content.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/cross-draft.json (renamed from tests/draft7/optional/cross-draft.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/ecmascript-regex.json (renamed from tests/draft7/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/float-overflow.json (renamed from tests/draft7/optional/float-overflow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/date-time.json (renamed from tests/draft7/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/date.json (renamed from tests/draft7/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/email.json (renamed from tests/draft7/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/hostname.json (renamed from tests/draft7/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/idn-email.json (renamed from tests/draft7/optional/format/idn-email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/idn-hostname.json (renamed from tests/draft7/optional/format/idn-hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/ipv4.json (renamed from tests/draft7/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/ipv6.json (renamed from tests/draft7/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/iri-reference.json (renamed from tests/draft7/optional/format/iri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/iri.json (renamed from tests/draft7/optional/format/iri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/json-pointer.json (renamed from tests/draft7/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/regex.json (renamed from tests/draft7/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/relative-json-pointer.json (renamed from tests/draft7/optional/format/relative-json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/time.json (renamed from tests/draft7/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/unknown.json (renamed from tests/draft7/optional/format/unknown.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri-reference.json (renamed from tests/draft7/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri-template.json (renamed from tests/draft7/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri.json (renamed from tests/draft7/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/non-bmp-regex.json (renamed from tests/draft7/optional/non-bmp-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/pattern.json (renamed from tests/draft7/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/patternProperties.json (renamed from tests/draft7/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/properties.json (renamed from tests/draft7/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/propertyNames.json (renamed from tests/draft7/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/ref.json (renamed from tests/draft7/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/refRemote.json (renamed from tests/draft7/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/required.json (renamed from tests/draft7/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/type.json (renamed from tests/draft7/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/uniqueItems.json (renamed from tests/draft7/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/unknownKeyword.json (renamed from tests/draft7/unknownKeyword.json) | 0 | ||||
l--------- | json/tests/latest (renamed from tests/latest) | 0 | ||||
-rw-r--r-- | json/tox.ini | 9 | ||||
-rw-r--r-- | jsonschema/__init__.py | 116 | ||||
-rw-r--r-- | jsonschema/__main__.py | 6 | ||||
-rw-r--r-- | jsonschema/_format.py | 522 | ||||
-rw-r--r-- | jsonschema/_legacy_validators.py | 300 | ||||
-rw-r--r-- | jsonschema/_types.py | 198 | ||||
-rw-r--r-- | jsonschema/_typing.py | 28 | ||||
-rw-r--r-- | jsonschema/_utils.py | 332 | ||||
-rw-r--r-- | jsonschema/_validators.py | 449 | ||||
-rw-r--r-- | jsonschema/benchmarks/__init__.py | 5 | ||||
-rw-r--r-- | jsonschema/benchmarks/issue232.py | 25 | ||||
-rw-r--r-- | jsonschema/benchmarks/issue232/issue.json | 2653 | ||||
-rw-r--r-- | jsonschema/benchmarks/json_schema_test_suite.py | 12 | ||||
-rw-r--r-- | jsonschema/benchmarks/validator_creation.py | 14 | ||||
-rw-r--r-- | jsonschema/cli.py | 300 | ||||
-rw-r--r-- | jsonschema/exceptions.py | 426 | ||||
-rw-r--r-- | jsonschema/protocols.py | 230 | ||||
-rw-r--r-- | jsonschema/tests/__init__.py | 0 | ||||
-rw-r--r-- | jsonschema/tests/_helpers.py | 25 | ||||
-rw-r--r-- | jsonschema/tests/_suite.py | 274 | ||||
-rw-r--r-- | jsonschema/tests/fuzz_validate.py | 50 | ||||
-rw-r--r-- | jsonschema/tests/test_cli.py | 916 | ||||
-rw-r--r-- | jsonschema/tests/test_deprecations.py | 299 | ||||
-rw-r--r-- | jsonschema/tests/test_exceptions.py | 601 | ||||
-rw-r--r-- | jsonschema/tests/test_format.py | 109 | ||||
-rw-r--r-- | jsonschema/tests/test_jsonschema_test_suite.py | 257 | ||||
-rw-r--r-- | jsonschema/tests/test_types.py | 221 | ||||
-rw-r--r-- | jsonschema/tests/test_utils.py | 124 | ||||
-rw-r--r-- | jsonschema/tests/test_validators.py | 2359 | ||||
-rw-r--r-- | jsonschema/validators.py | 1335 | ||||
-rw-r--r-- | pyproject.toml | 153 | ||||
-rw-r--r-- | tox.ini | 116 |
588 files changed, 15940 insertions, 24 deletions
diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..5c6eb7e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +# vim: filetype=dosini: +[run] +branch = True +source = jsonschema +omit = */jsonschema/_reflect.py,*/jsonschema/__main__.py,*/jsonschema/benchmarks/*,*/jsonschema/tests/fuzz_validate.py + +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..39a1618 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: "Julian" +tidelift: "pypi/jsonschema" diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..fd524e9 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +In general, only the latest released ``jsonschema`` version is supported +and will receive updates. + +## Reporting a Vulnerability + +To report a security vulnerability, please send an email to +``Julian+Security@GrayVines.com`` with subject line ``SECURITY +(jsonschema)``. + +I will do my best to respond within 48 hours to acknowledge the message +and discuss further steps. + +If the vulnerability is accepted, an advisory will be sent out via +GitHub's security advisory functionality. + +For non-sensitive discussion related to this policy itself, feel free to +open an issue on the issue tracker. diff --git a/.github/coverage.sh b/.github/coverage.sh new file mode 100644 index 0000000..d267c96 --- /dev/null +++ b/.github/coverage.sh @@ -0,0 +1,3 @@ +set -e +printf '### Coverage\n\n' >>$GITHUB_STEP_SUMMARY +"$1" -m coverage report --format=markdown --show-missing >>$GITHUB_STEP_SUMMARY diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..9d1e098 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a826069..06208ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test Suite Sanity Checking +name: CI on: push: @@ -6,20 +6,205 @@ on: release: types: [published] schedule: - # Daily at 6:42, arbitrarily as a time that's possibly non-busy - - cron: '42 6 * * *' + # Daily at 3:21 + - cron: '21 3 * * *' jobs: - ci: + pre-commit: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - uses: pre-commit/action@v3.0.0 + + ci: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: + - name: pypy-3.9 + toxenv: pypy3-noextra-build + - name: pypy-3.9 + toxenv: pypy3-noextra-tests + - name: pypy-3.9 + toxenv: pypy3-format-build + - name: pypy-3.9 + toxenv: pypy3-format-tests + - name: pypy-3.9 + toxenv: pypy3-formatnongpl-build + - name: pypy-3.9 + toxenv: pypy3-formatnongpl-tests + - name: 3.8 + toxenv: py38-noextra-build + - name: 3.8 + toxenv: py38-noextra-tests + - name: 3.8 + toxenv: py38-format-build + - name: 3.8 + toxenv: py38-format-tests + - name: 3.8 + toxenv: py38-formatnongpl-build + - name: 3.8 + toxenv: py38-formatnongpl-tests + - name: 3.9 + toxenv: py39-noextra-build + - name: 3.9 + toxenv: py39-noextra-tests + - name: 3.9 + toxenv: py39-format-build + - name: 3.9 + toxenv: py39-format-tests + - name: 3.9 + toxenv: py39-formatnongpl-build + - name: 3.9 + toxenv: py39-formatnongpl-tests + - name: "3.10" + toxenv: py310-noextra-build + - name: "3.10" + toxenv: py310-noextra-tests + - name: "3.10" + toxenv: py310-format-build + - name: "3.10" + toxenv: py310-format-tests + - name: "3.10" + toxenv: py310-formatnongpl-build + - name: "3.10" + toxenv: py310-formatnongpl-tests + - name: "3.11" + toxenv: py311-noextra-build + - name: "3.11" + toxenv: py311-noextra-tests + - name: "3.11" + toxenv: py311-format-build + - name: "3.11" + toxenv: py311-format-tests + - name: "3.11" + toxenv: py311-formatnongpl-build + - name: "3.11" + toxenv: py311-formatnongpl-tests + - name: "3.11" + toxenv: docs-dirhtml + - name: "3.11" + toxenv: docs-doctest + - name: "3.11" + toxenv: docs-linkcheck + - name: "3.11" + toxenv: docs-spelling + - name: "3.11" + toxenv: docs-style + - name: "3.11" + toxenv: readme + - name: "3.11" + toxenv: secrets + - name: "3.11" + toxenv: style + - name: "3.11" + toxenv: typing + include: + - os: ubuntu-latest + python-version: + name: "3.11" + toxenv: py311-format-ghcoverage + - os: ubuntu-latest + python-version: + name: "3.11" + toxenv: py311-noextra-ghcoverage + - os: ubuntu-latest + python-version: + name: "3.11" + toxenv: format-audit + - os: ubuntu-latest + python-version: + name: "3.11" + toxenv: formatnongpl-audit + - os: ubuntu-latest + python-version: + name: "3.11" + toxenv: noextra-audit + exclude: + - os: windows-latest + python-version: + name: "3.11" + toxenv: readme + - os: windows-latest + python-version: + name: "3.11" + toxenv: docs-dirhtml + - os: windows-latest + python-version: + name: "3.11" + toxenv: docs-doctest + - os: windows-latest + python-version: + name: "3.11" + toxenv: docs-linkcheck + - os: windows-latest + python-version: + name: "3.11" + toxenv: docs-spelling + - os: windows-latest + python-version: + name: "3.11" + toxenv: docs-style steps: - uses: actions/checkout@v3 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version.name }} uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: ${{ matrix.python-version.name }} + - name: Install dependencies + run: > + sudo apt-get update && + sudo apt-get install -y libenchant-2-dev libxml2-dev libxslt-dev + if: runner.os == 'Linux' && startsWith(matrix.python-version.toxenv, 'docs-') + - name: Install dependencies + run: brew install enchant + if: runner.os == 'macOS' && startsWith(matrix.python-version.toxenv, 'docs-') - name: Install tox run: python -m pip install tox - - name: Run the sanity checks - run: python -m tox + - name: Enable UTF-8 on Windows + run: echo "PYTHONUTF8=1" >> $env:GITHUB_ENV + if: runner.os == 'Windows' && startsWith(matrix.python-version.toxenv, 'py') + - name: Run tox + run: python -m tox -e "${{ matrix.python-version.toxenv }}" + + packaging: + needs: ci + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install dependencies + run: python -m pip install build + - name: Create packages + run: python -m build . + - uses: actions/upload-artifact@v3 + with: + name: dist + path: dist + - name: Publish package + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.pypi_password }} + - name: Create Release Notes + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.request(`POST /repos/${{ github.repository }}/releases`, { + tag_name: "${{ github.ref }}", + generate_release_notes: true + }); diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml new file mode 100644 index 0000000..5757faf --- /dev/null +++ b/.github/workflows/documentation-links.yml @@ -0,0 +1,16 @@ +name: Read the Docs Pull Request Preview +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + documentation-links: + runs-on: ubuntu-latest + steps: + - uses: readthedocs/actions/preview@v1 + with: + project-slug: "python-jsonschema" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..dcc49a3 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,30 @@ +name: CIFuzz + +on: + pull_request: + branches: + - main + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'jsonschema' + language: python + continue-on-error: true + - name: Run Fuzzers + if: steps.build.outcome == 'success' + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'jsonschema' + fuzz-seconds: 30 + - name: Upload Crash + uses: actions/upload-artifact@v3 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts @@ -101,15 +101,7 @@ ipython_config.py # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +# PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff @@ -153,8 +145,13 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# User defined +_cache +_static +_templates diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c762ea3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +exclude: json/ + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-ast + - id: check-json + - id: check-toml + - id: check-vcs-permalinks + - id: check-yaml + - id: debug-statements + exclude: '^jsonschema/tests/_suite.py$' + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix, lf] + - id: trailing-whitespace + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..f806715 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: jsonschema + name: jsonschema + description: json schema validation + language: python + pass_filenames: false + entry: jsonschema diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3104388 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +version: 2 + +build: + os: ubuntu-22.04 + apt_packages: + - inkscape + tools: + python: "3.11" + +sphinx: + builder: dirhtml + configuration: docs/conf.py + fail_on_warning: true + +formats: all + +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..fd4698f --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,517 @@ +v4.18.0 +======= + +This release majorly rehaul's the way in which JSON Schema reference resolution is configured. +It does so in a way that *should* be backwards compatible, preserving old behavior whilst emitting deprecation warnings. + +* ``jsonschema.RefResolver`` is now deprecated in favor of the new `referencing library <https://github.com/python-jsonschema/referencing/>`_. + ``referencing`` will begin in beta, but already is more compliant than the existing ``$ref`` support. + This change is a culmination of a meaningful chunk of work to make ``$ref`` resolution more flexible and more correct. + Backwards compatibility *should* be preserved for existing code which uses ``RefResolver``, though doing so is again now deprecated, and all such use cases should be doable using the new APIs. + Please file issues on the ``referencing`` tracker if there is functionality missing from it, or here on the ``jsonschema`` issue tracker if you have issues with existing code not functioning the same, or with figuring out how to change it to use ``referencing``. +* Support for Python 3.7 has been dropped, as it is nearing end-of-life. + This should not be a "visible" change in the sense that ``requires-python`` has been updated, so users using 3.7 should still receive ``v4.17.3`` when installing the library. +* On draft 2019-09, ``unevaluatedItems`` now properly does *not* consider items to be evaluated by an ``additionalItems`` schema if ``items`` is missing from the schema, as the specification says in this case that ``additionalItems`` must be completely ignored. + +Deprecations +------------ + +* ``jsonschema.RefResolver`` -- see above for details on the replacement +* ``jsonschema.RefResolutionError`` -- see above for details on the replacement +* importing ``jsonschema.ErrorTree`` -- instead import it via ``jsonschema.exceptions.ErrorTree`` +* importing ``jsonschema.FormatError`` -- instead import it via ``jsonschema.exceptions.FormatError`` + +v4.17.3 +======= + +* Fix instantiating validators with cached refs to boolean schemas + rather than objects (#1018). + +v4.17.2 +======= + +* Empty strings are not valid relative JSON Pointers (aren't valid under the + RJP format). +* Durations without (trailing) units are not valid durations (aren't + valid under the duration format). This involves changing the dependency + used for validating durations (from ``isoduration`` to ``isodate``). + +v4.17.1 +======= + +* The error message when using ``unevaluatedProperties`` with a non-trivial + schema value (i.e. something other than ``false``) has been improved (#996). + +v4.17.0 +======= + +* The ``check_schema`` method on ``jsonschema.protocols.Validator`` instances + now *enables* format validation by default when run. This can catch some + additional invalid schemas (e.g. containing invalid regular expressions) + where the issue is indeed uncovered by validating against the metaschema + with format validation enabled as an assertion. +* The ``jsonschema`` CLI (along with ``jsonschema.cli`` the module) are now + deprecated. Use ``check-jsonschema`` instead, which can be installed via + ``pip install check-jsonschema`` and found + `here <https://github.com/python-jsonschema/check-jsonschema>`_. + +v4.16.1 +======= + +* Make ``ErrorTree`` have a more grammatically correct ``repr``. + +v4.16.0 +======= + +* Improve the base URI behavior when resolving a ``$ref`` to a resolution URI + which is different from the resolved schema's declared ``$id``. +* Accessing ``jsonschema.draftN_format_checker`` is deprecated. Instead, if you + want access to the format checker itself, it is exposed as + ``jsonschema.validators.DraftNValidator.FORMAT_CHECKER`` on any + ``jsonschema.protocols.Validator``. + +v4.15.0 +======= + +* A specific API Reference page is now present in the documentation. +* ``$ref`` on earlier drafts (specifically draft 7 and 6) has been "fixed" to + follow the specified behavior when present alongside a sibling ``$id``. + Specifically the ID is now properly ignored, and references are resolved + against whatever resolution scope was previously relevant. + +v4.14.0 +======= + +* ``FormatChecker.cls_checks`` is deprecated. Use ``FormatChecker.checks`` on + an instance of ``FormatChecker`` instead. +* ``unevaluatedItems`` has been fixed for draft 2019. It's nonetheless + discouraged to use draft 2019 for any schemas, new or old. +* Fix a number of minor annotation issues in ``protocols.Validator`` + +v4.13.0 +======= + +* Add support for creating validator classes whose metaschema uses a different + dialect than its schemas. In other words, they may use draft2020-12 to define + which schemas are valid, but the schemas themselves use draft7 (or a custom + dialect, etc.) to define which *instances* are valid. Doing this is likely + not something most users, even metaschema authors, may need, but occasionally + will be useful for advanced use cases. + +v4.12.1 +======= + +* Fix some stray comments in the README. + +v4.12.0 +======= + +* Warn at runtime when subclassing validator classes. Doing so was not + intended to be public API, though it seems some downstream libraries + do so. A future version will make this an error, as it is brittle and + better served by composing validator objects instead. Feel free to reach + out if there are any cases where changing existing code seems difficult + and I can try to provide guidance. + +v4.11.0 +======= + +* Make the rendered README in PyPI simpler and fancier. Thanks Hynek (#983)! + +v4.10.3 +======= + +* ``jsonschema.validators.validator_for`` now properly uses the explicitly + provided default validator even if the ``$schema`` URI is not found. + +v4.10.2 +======= + +* Fix a second place where subclasses may have added attrs attributes (#982). + +v4.10.1 +======= + +* Fix Validator.evolve (and APIs like ``iter_errors`` which call it) for cases + where the validator class has been subclassed. Doing so wasn't intended to be + public API, but given it didn't warn or raise an error it's of course + understandable. The next release however will make it warn (and a future one + will make it error). If you need help migrating usage of inheriting from a + validator class feel free to open a discussion and I'll try to give some + guidance (#982). + +v4.10.0 +======= + +* Add support for referencing schemas with ``$ref`` across different versions + of the specification than the referrer's + +v4.9.1 +====== + +* Update some documentation examples to use newer validator releases in their + sample code. + +v4.9.0 +====== + +* Fix relative ``$ref`` resolution when the base URI is a URN or other scheme + (#544). +* ``pkgutil.resolve_name`` is now used to retrieve validators + provided on the command line. This function is only available on + 3.9+, so 3.7 and 3.8 (which are still supported) now rely on the + `pkgutil_resolve_name <https://pypi.org/project/pkgutil_resolve_name/>`_ + backport package. Note however that the CLI itself is due + to be deprecated shortly in favor of `check-jsonschema + <https://github.com/python-jsonschema/check-jsonschema>`_. + +v4.8.0 +====== + +* ``best_match`` no longer traverses into ``anyOf`` and ``oneOf`` when all of + the errors within them seem equally applicable. This should lead to clearer + error messages in some cases where no branches were matched. + +v4.7.2 +====== + +* Also have ``best_match`` handle cases where the ``type`` validator is an + array. + +v4.7.1 +====== + +* Minor tweak of the PyPI hyperlink names + +v4.7.0 +====== + +* Enhance ``best_match`` to prefer errors from branches of the schema which + match the instance's type (#728) + +v4.6.2 +====== + +* Fix a number of minor typos in docstrings, mostly private ones (#969) + +v4.6.1 +====== + +* Gut the (incomplete) implementation of ``recursiveRef`` on draft 2019. It + needs completing, but for now can lead to recursion errors (e.g. #847). + +v4.6.0 +====== + +* Fix ``unevaluatedProperties`` and ``unevaluatedItems`` for types they should + ignore (#949) +* ``jsonschema`` now uses `hatch <https://hatch.pypa.io/>`_ for its build + process. This should be completely transparent to end-users (and only matters + to contributors). + +v4.5.1 +====== + +* Revert changes to ``$dynamicRef`` which caused a performance regression + in v4.5.0 + +v4.5.0 +====== + +* Validator classes for each version now maintain references to the correct + corresponding format checker (#905) +* Development has moved to a `GitHub organization + <https://github.com/python-jsonschema/>`_. + No functional behavior changes are expected from the change. + +v4.4.0 +====== + +* Add ``mypy`` support (#892) +* Add support for Python 3.11 + +v4.3.3 +====== + +* Properly report deprecation warnings at the right stack level (#899) + +v4.3.2 +====== + +* Additional performance improvements for resolving refs (#896) + +v4.3.1 +====== + +* Resolving refs has had performance improvements (#893) + +v4.3.0 +====== + +* Fix undesired fallback to brute force container uniqueness check on + certain input types (#893) +* Implement a PEP544 Protocol for validator classes (#890) + +v4.2.1 +====== + +* Pin ``importlib.resources`` from below (#877) + +v4.2.0 +====== + +* Use ``importlib.resources`` to load schemas (#873) +* Ensure all elements of arrays are verified for uniqueness by ``uniqueItems`` + (#866) + +v4.1.2 +====== + +* Fix ``dependentSchemas`` to properly consider non-object instances to be + valid (#850) + +v4.1.1 +====== + +* Fix ``prefixItems`` not indicating which item was invalid within the instance + path (#862) + +v4.1.0 +====== + +* Add Python 3.10 to the list of supported Python versions + +v4.0.1 +====== + +* Fix the declaration of minimum supported Python version (#846) + +v4.0.0 +====== + +* Partial support for Draft 2020-12 (as well as 2019-09). + Thanks to Thomas Schmidt and Harald Nezbeda. +* ``False`` and ``0`` are now properly considered non-equal even + recursively within a container (#686). As part of this change, + ``uniqueItems`` validation may be *slower* in some cases. Please feel + free to report any significant performance regressions, though in + some cases they may be difficult to address given the specification + requirement. +* The CLI has been improved, and in particular now supports a ``--output`` + option (with ``plain`` (default) or ``pretty`` arguments) to control the + output format. Future work may add additional machine-parsable output + formats. +* Code surrounding ``DEFAULT_TYPES`` and the legacy mechanism for + specifying types to validators have been removed, as per the deprecation + policy. Validators should use the ``TypeChecker`` object to customize + the set of Python types corresponding to JSON Schema types. +* Validation errors now have a ``json_path`` attribute, describing their + location in JSON path format +* Support for the IP address and domain name formats has been improved +* Support for Python 2 and 3.6 has been dropped, with ``python_requires`` + properly set. +* ``multipleOf`` could overflow when given sufficiently large numbers. Now, + when an overflow occurs, ``jsonschema`` will fall back to using fraction + division (#746). +* ``jsonschema.__version__``, ``jsonschema.validators.validators``, + ``jsonschema.validators.meta_schemas`` and + ``jsonschema.RefResolver.in_scope`` have been deprecated, as has + passing a second-argument schema to ``Validator.iter_errors`` and + ``Validator.is_valid``. + +v3.2.0 +====== + +* Added a ``format_nongpl`` setuptools extra, which installs only ``format`` + dependencies that are non-GPL (#619). + +v3.1.1 +====== + +* Temporarily revert the switch to ``js-regex`` until #611 and #612 are + resolved. + +v3.1.0 +====== + +* Regular expressions throughout schemas now respect the ECMA 262 dialect, as + recommended by the specification (#609). + +v3.0.2 +====== + +* Fixed a bug where ``0`` and ``False`` were considered equal by + ``const`` and ``enum`` (#575). + +v3.0.1 +====== + +* Fixed a bug where extending validators did not preserve their notion + of which validator property contains ``$id`` information. + +v3.0.0 +====== + +* Support for Draft 6 and Draft 7 +* Draft 7 is now the default +* New ``TypeChecker`` object for more complex type definitions (and overrides) +* Falling back to isodate for the date-time format checker is no longer + attempted, in accordance with the specification + +v2.6.0 +====== + +* Support for Python 2.6 has been dropped. +* Improve a few error messages for ``uniqueItems`` (#224) and + ``additionalProperties`` (#317) +* Fixed an issue with ``ErrorTree``'s handling of multiple errors (#288) + +v2.5.0 +====== + +* Improved performance on CPython by adding caching around ref resolution + (#203) + +v2.4.0 +====== + +* Added a CLI (#134) +* Added absolute path and absolute schema path to errors (#120) +* Added ``relevance`` +* Meta-schemas are now loaded via ``pkgutil`` + +v2.3.0 +====== + +* Added ``by_relevance`` and ``best_match`` (#91) +* Fixed ``format`` to allow adding formats for non-strings (#125) +* Fixed the ``uri`` format to reject URI references (#131) + +v2.2.0 +====== + +* Compile the host name regex (#127) +* Allow arbitrary objects to be types (#129) + +v2.1.0 +====== + +* Support RFC 3339 datetimes in conformance with the spec +* Fixed error paths for additionalItems + items (#122) +* Fixed wording for min / maxProperties (#117) + + +v2.0.0 +====== + +* Added ``create`` and ``extend`` to ``jsonschema.validators`` +* Removed ``ValidatorMixin`` +* Fixed array indices ref resolution (#95) +* Fixed unknown scheme defragmenting and handling (#102) + + +v1.3.0 +====== + +* Better error tracebacks (#83) +* Raise exceptions in ``ErrorTree``\s for keys not in the instance (#92) +* __cause__ (#93) + + +v1.2.0 +====== + +* More attributes for ValidationError (#86) +* Added ``ValidatorMixin.descend`` +* Fixed bad ``RefResolutionError`` message (#82) + + +v1.1.0 +====== + +* Canonicalize URIs (#70) +* Allow attaching exceptions to ``format`` errors (#77) + + +v1.0.0 +====== + +* Support for Draft 4 +* Support for format +* Longs are ints too! +* Fixed a number of issues with ``$ref`` support (#66) +* Draft4Validator is now the default +* ``ValidationError.path`` is now in sequential order +* Added ``ValidatorMixin`` + + +v0.8.0 +====== + +* Full support for JSON References +* ``validates`` for registering new validators +* Documentation +* Bugfixes + + * uniqueItems not so unique (#34) + * Improper any (#47) + + +v0.7 +==== + +* Partial support for (JSON Pointer) ``$ref`` +* Deprecations + + * ``Validator`` is replaced by ``Draft3Validator`` with a slightly different + interface + * ``validator(meta_validate=False)`` + + +v0.6 +==== + +* Bugfixes + + * Issue #30 - Wrong behavior for the dependencies property validation + * Fixed a miswritten test + + +v0.5 +==== + +* Bugfixes + + * Issue #17 - require path for error objects + * Issue #18 - multiple type validation for non-objects + + +v0.4 +==== + +* Preliminary support for programmatic access to error details (Issue #5). + There are certainly some corner cases that don't do the right thing yet, but + this works mostly. + + In order to make this happen (and also to clean things up a bit), a number + of deprecations are necessary: + + * ``stop_on_error`` is deprecated in ``Validator.__init__``. Use + ``Validator.iter_errors()`` instead. + * ``number_types`` and ``string_types`` are deprecated there as well. + Use ``types={"number" : ..., "string" : ...}`` instead. + * ``meta_validate`` is also deprecated, and instead is now accepted as + an argument to ``validate``, ``iter_errors`` and ``is_valid``. + +* A bugfix or two + + +v0.3 +==== + +* Default for unknown types and properties is now to *not* error (consistent + with the schema). +* Python 3 support +* Removed dependency on SecureTypes now that the hash bug has been resolved. +* "Numerous bug fixes" -- most notably, a divisibleBy error for floats and a + bunch of missing typechecks for irrelevant properties. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..c051d56 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,60 @@ +============================ +Contributing to `jsonschema` +============================ + +Found a bug? +------------ + +If you suspect you may have found a security-related vulnerability, please follow the instructions in `the security policy <https://github.com/python-jsonschema/jsonschema/blob/main/.github/SECURITY.md>`_. + +Otherwise, it is extremely helpful if you first search to see whether your bug has been `previously reported on the Issues tab <https://github.com/python-jsonschema/jsonschema/issues?q=is%3Aissue+is%3Aopen+label%3ABug>`_. + +If it doesn't appear to be a known issue, please `file a new one <https://github.com/python-jsonschema/jsonschema/issues/new>`_, and include a **title and clear description**, along with as much relevant information as possible. +Including a *minimal*, *self-sufficient* bit of code (often an instance and schema) is the fastest way to get attention, along with a description of the behavior you expect, and if you're able, a link to where in the specification contains the behavior you're noticing is incorrect. + +Pull requests to fix your issue are of course very welcome. + + +Fixing a Bug? +------------- + +Please open a new GitHub pull request with the change, along with new tests. + +Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +Continuous integration via GitHub actions should run to indicate whether your change passes both the test suite as well as linters. +Please ensure it passes, or indicate in a comment if you believe it fails spuriously. + + +Adding New Functionality? +------------------------- + +Please discuss any larger changes ahead of time for the sake of your own time! + +Improvements are very welcome, but large pull requests, disruptive ones, or backwards incompatible ones, can lead to long back and forth discussions. + +You're welcome to suggest a change in an issue and thereby get some initial feedback before embarking on an effort that may not get merged. + + +Improving the Documentation? +---------------------------- + +Writing good documentation is challenging both to prioritize and to do well. + +Any help you may have would be great, especially if you're a beginner who's struggled to understand a part of the library. + +Documentation is written in `Sphinx-flavored reStructuredText <https://www.sphinx-doc.org>`_, so you'll want to familiarize yourself a bit with Sphinx. + +Feel free to file issues or pull requests. + + +Have a Question? +---------------- + +Please do not use the issue tracker for questions, it's reserved for things believed to be bugs, or new functionality. + +There is a `discussions tab <https://github.com/python-jsonschema/jsonschema/discussions>`_ where general questions can be asked. + +Answers on it are best-effort. + +Any help you can offer to answer others' questions is of course very welcome as well. @@ -0,0 +1,19 @@ +Copyright (c) 2013 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ea97daa --- /dev/null +++ b/README.rst @@ -0,0 +1,141 @@ +========== +jsonschema +========== + +|PyPI| |Pythons| |CI| |ReadTheDocs| |Precommit| |Zenodo| + +.. |PyPI| image:: https://img.shields.io/pypi/v/jsonschema.svg + :alt: PyPI version + :target: https://pypi.org/project/jsonschema/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonschema.svg + :alt: Supported Python versions + :target: https://pypi.org/project/jsonschema/ + +.. |CI| image:: https://github.com/python-jsonschema/jsonschema/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/python-jsonschema/jsonschema/actions?query=workflow%3ACI + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/python-jsonschema/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://python-jsonschema.readthedocs.io/en/stable/ + +.. |Precommit| image:: https://results.pre-commit.ci/badge/github/python-jsonschema/jsonschema/main.svg + :alt: pre-commit.ci status + :target: https://results.pre-commit.ci/latest/github/python-jsonschema/jsonschema/main + +.. |Zenodo| image:: https://zenodo.org/badge/3072629.svg + :alt: Zenodo DOI + :target: https://zenodo.org/badge/latestdoi/3072629 + + +``jsonschema`` is an implementation of the `JSON Schema <https://json-schema.org>`_ specification for Python. + +.. code:: python + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate(instance={"name" : "Eggs", "price" : 34.99}, schema=schema) + + >>> validate( + ... instance={"name" : "Eggs", "price" : "Invalid"}, schema=schema, + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + +It can also be used from the command line by installing `check-jsonschema <https://github.com/python-jsonschema/check-jsonschema>`_. + +Features +-------- + +* Full support for `Draft 2020-12 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft202012Validator>`_, `Draft 2019-09 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft201909Validator>`_, `Draft 7 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft7Validator>`_, `Draft 6 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft6Validator>`_, `Draft 4 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft4Validator>`_ and `Draft 3 <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/validators/#jsonschema.validators.Draft3Validator>`_ + +* `Lazy validation <https://python-jsonschema.readthedocs.io/en/latest/api/jsonschema/protocols/#jsonschema.protocols.Validator.iter_errors>`_ that can iteratively report *all* validation errors. + +* `Programmatic querying <https://python-jsonschema.readthedocs.io/en/latest/errors/>`_ of which properties or items failed validation. + + +Installation +------------ + +``jsonschema`` is available on `PyPI <https://pypi.org/project/jsonschema/>`_. You can install using `pip <https://pip.pypa.io/en/stable/>`_: + +.. code:: bash + + $ pip install jsonschema + + +Extras +====== + +Two extras are available when installing the package, both currently related to ``format`` validation: + + * ``format`` + * ``format-nongpl`` + +They can be used when installing in order to include additional dependencies, e.g.: + +.. code:: bash + + $ pip install jsonschema'[format]' + +Be aware that the mere presence of these dependencies – or even the specification of ``format`` checks in a schema – do *not* activate format checks (as per the specification). +Please read the `format validation documentation <https://python-jsonschema.readthedocs.io/en/latest/validate/#validating-formats>`_ for further details. + +.. start cut from PyPI + +Running the Test Suite +---------------------- + +If you have ``tox`` installed (perhaps via ``pip install tox`` or your package manager), running ``tox`` in the directory of your source checkout will run ``jsonschema``'s test suite on all of the versions of Python ``jsonschema`` supports. +If you don't have all of the versions that ``jsonschema`` is tested under, you'll likely want to run using ``tox``'s ``--skip-missing-interpreters`` option. + +Of course you're also free to just run the tests on a single version with your favorite test runner. +The tests live in the ``jsonschema.tests`` package. + + +Benchmarks +---------- + +``jsonschema``'s benchmarks make use of `pyperf <https://pyperf.readthedocs.io>`_. +Running them can be done via:: + + $ tox -e perf + + +Community +--------- + +The JSON Schema specification has `a Slack <https://json-schema.slack.com>`_, with an `invite link on its home page <https://json-schema.org/>`_. +Many folks knowledgeable on authoring schemas can be found there. + +Otherwise, opening a `GitHub discussion <https://github.com/python-jsonschema/jsonschema/discussions>`_ or asking questions on Stack Overflow are other means of getting help if you're stuck. + +.. end cut from PyPI + + +About +----- + +I'm Julian Berman. + +``jsonschema`` is on `GitHub <https://github.com/python-jsonschema/jsonschema>`_. + +Get in touch, via GitHub or otherwise, if you've got something to contribute, it'd be most welcome! + +You can also generally find me on Libera (nick: ``Julian``) in various channels, including ``#python``. + +If you feel overwhelmingly grateful, you can also `sponsor me <https://github.com/sponsors/Julian/>`_. + +And for companies who appreciate ``jsonschema`` and its continued support and growth, ``jsonschema`` is also now supportable via `TideLift <https://tidelift.com/subscription/pkg/pypi-jsonschema?utm_source=pypi-jsonschema&utm_medium=referral&utm_campaign=readme>`_. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..640bd89 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + patch: + default: + target: 100% diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..f6315df --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,227 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +PYTHON = python +PAPER = +BUILDDIR = _build +SOURCEDIR = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " spelling to run a spell check of the documentation" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(PYTHON) -m sphinx -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(PYTHON) -m sphinx -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(PYTHON) -m sphinx -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: json +json: + $(PYTHON) -m sphinx -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(PYTHON) -m sphinx -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(PYTHON) -m sphinx -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jsonschema.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jsonschema.qhc" + +.PHONY: applehelp +applehelp: + $(PYTHON) -m sphinx -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(PYTHON) -m sphinx -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/jsonschema" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jsonschema" + @echo "# devhelp" + +.PHONY: epub +epub: + $(PYTHON) -m sphinx -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(PYTHON) -m sphinx -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(PYTHON) -m sphinx -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(PYTHON) -m sphinx -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(PYTHON) -m sphinx -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(PYTHON) -m sphinx -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(PYTHON) -m sphinx -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(PYTHON) -m sphinx -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(PYTHON) -m sphinx -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(PYTHON) -m sphinx -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(PYTHON) -m sphinx -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(PYTHON) -m sphinx -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(PYTHON) -m sphinx -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: spelling +spelling: + $(PYTHON) -m sphinx -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling + @echo + @echo "Build finished. The spelling files are in $(BUILDDIR)/spelling." + +.PHONY: dummy +dummy: + $(PYTHON) -m sphinx -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..6624e83 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,24 @@ +API Reference +============= + +Submodules +---------- + +.. toctree:: + :titlesonly: + + /api/jsonschema/validators/index + /api/jsonschema/exceptions/index + /api/jsonschema/protocols/index + +:mod:`jsonschema` +----------------- + +.. automodule:: jsonschema + :members: + :imported-members: + :exclude-members: Validator + +.. autodata:: jsonschema._format._F + +.. autodata:: jsonschema._typing.id_of diff --git a/docs/api/jsonschema/exceptions/index.rst b/docs/api/jsonschema/exceptions/index.rst new file mode 100644 index 0000000..8fb1f4f --- /dev/null +++ b/docs/api/jsonschema/exceptions/index.rst @@ -0,0 +1,6 @@ +:py:mod:`jsonschema.exceptions` +=============================== + +.. automodule:: jsonschema.exceptions + :members: + :undoc-members: diff --git a/docs/api/jsonschema/protocols/index.rst b/docs/api/jsonschema/protocols/index.rst new file mode 100644 index 0000000..195dbee --- /dev/null +++ b/docs/api/jsonschema/protocols/index.rst @@ -0,0 +1,6 @@ +:py:mod:`jsonschema.protocols` +============================== + +.. automodule:: jsonschema.protocols + :members: + :undoc-members: diff --git a/docs/api/jsonschema/validators/index.rst b/docs/api/jsonschema/validators/index.rst new file mode 100644 index 0000000..13a9991 --- /dev/null +++ b/docs/api/jsonschema/validators/index.rst @@ -0,0 +1,7 @@ +:py:mod:`jsonschema.validators` +=============================== + +.. automodule:: jsonschema.validators + :members: + :undoc-members: + :private-members: _RefResolver diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..19d734f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,136 @@ +from pathlib import Path +import importlib.metadata +import re + +ROOT = Path(__file__).parent.parent +PACKAGE_SRC = ROOT / "jsonschema" + +project = "jsonschema" +author = "Julian Berman" +copyright = "2013, " + author + +release = importlib.metadata.version("jsonschema") +version = release.partition("-")[0] + +language = "en" +default_role = "any" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosectionlabel", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.imgconverter", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_json_schema_spec", + "sphinxcontrib.spelling", + "sphinxext.opengraph", +] + +cache_path = "_cache" + +pygments_style = "lovelace" +pygments_dark_style = "one-dark" + +html_theme = "furo" + +# See sphinx-doc/sphinx#10785 +_TYPE_ALIASES = { + "jsonschema._format._F": ("data", "_F"), + "_typing.id_of": ("data", "jsonschema._typing.id_of"), +} + + +def _resolve_broken_refs(app, env, node, contnode): + if node["refdomain"] != "py": + return + + if node["reftarget"].startswith("referencing."): # :( :( :( :( :( + node["reftype"] = "data" + from sphinx.ext import intersphinx + return intersphinx.resolve_reference_in_inventory( + env, "referencing", node, contnode, + ) + + kind, target = _TYPE_ALIASES.get(node["reftarget"], (None, None)) + if kind is not None: + return app.env.get_domain("py").resolve_xref( + env, + node["refdoc"], + app.builder, + kind, + target, + node, + contnode, + ) + + +def setup(app): + app.connect("missing-reference", _resolve_broken_refs) + + +# = Builders = + +doctest_global_setup = """ +from jsonschema import * +from jsonschema import exceptions +import jsonschema.validators +""" + + +def entire_domain(host): + return r"http.?://" + re.escape(host) + r"($|/.*)" + + +linkcheck_ignore = [ + entire_domain("img.shields.io"), + "https://github.com/python-jsonschema/jsonschema/actions", + "https://github.com/python-jsonschema/jsonschema/workflows/CI/badge.svg", +] + +# = Extensions = + +# -- autoapi -- + +suppress_warnings = [ + "autoapi.python_import_resolution", + "autoapi.toc_reference", + "epub.duplicated_toc_entry", +] +autoapi_root = "api" +autoapi_ignore = [ + "*/_[a-z]*.py", + "*/__main__.py", + "*/benchmarks/*", + "*/cli.py", + "*/tests/*", +] +autoapi_options = [ + "members", + "undoc-members", + "show-module-summary", + "imported-members", +] + +autoapi_type = "python" +autoapi_dirs = [PACKAGE_SRC] + +# -- autosectionlabel -- + +autosectionlabel_prefix_document = True + +# -- intersphinx -- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "referencing": ("https://referencing.readthedocs.io/en/stable/", None), + "ujs": ("https://json-schema.org/understanding-json-schema/", None), +} + +# -- sphinxcontrib-spelling -- + +spelling_word_list_filename = "spelling-wordlist.txt" +spelling_show_suggestions = True diff --git a/docs/creating.rst b/docs/creating.rst new file mode 100644 index 0000000..8405f34 --- /dev/null +++ b/docs/creating.rst @@ -0,0 +1,38 @@ +.. currentmodule:: jsonschema.validators + +.. _creating-validators: + +======================================= +Creating or Extending Validator Classes +======================================= + +.. autofunction:: create + :noindex: + +.. autofunction:: extend + :noindex: + +.. autofunction:: validator_for + :noindex: + +.. autofunction:: validates + :noindex: + + +Creating Validation Errors +-------------------------- + +Any validating function that validates against a subschema should call +``descend``, rather than ``iter_errors``. If it recurses into the +instance, or schema, it should pass one or both of the ``path`` or +``schema_path`` arguments to ``descend`` in order to properly maintain +where in the instance or schema respectively the error occurred. + +The Validator Protocol +---------------------- + +``jsonschema`` defines a `protocol <typing.Protocol>`, +`jsonschema.protocols.Validator` which can be used in type annotations to +describe the type of a validator object. + +For full details, see `validator-protocol`. diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..5b0230f --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,412 @@ +========================== +Handling Validation Errors +========================== + +.. currentmodule:: jsonschema.exceptions + +When an invalid instance is encountered, a `ValidationError` will be +raised or returned, depending on which method or function is used. + +.. autoexception:: ValidationError + :noindex: + + The information carried by an error roughly breaks down into: + + =============== ================= ======================== + What Happened Why Did It Happen What Was Being Validated + =============== ================= ======================== + `message` `context` `instance` + + `cause` `json_path` + + `path` + + `schema` + + `schema_path` + + `validator` + + `validator_value` + =============== ================= ======================== + + + .. attribute:: message + + A human readable message explaining the error. + + .. attribute:: validator + + The name of the failed `keyword + <https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural>`_. + + .. attribute:: validator_value + + The associated value for the failed keyword in the schema. + + .. attribute:: schema + + The full schema that this error came from. This is potentially a + subschema from within the schema that was passed in originally, + or even an entirely different schema if a :kw:`$ref` was + followed. + + .. attribute:: relative_schema_path + + A `collections.deque` containing the path to the failed keyword + within the schema. + + .. attribute:: absolute_schema_path + + A `collections.deque` containing the path to the failed + keyword within the schema, but always relative to the + *original* schema as opposed to any subschema (i.e. the one + originally passed into a validator class, *not* `schema`\). + + .. attribute:: schema_path + + Same as `relative_schema_path`. + + .. attribute:: relative_path + + A `collections.deque` containing the path to the + offending element within the instance. The deque can be empty if + the error happened at the root of the instance. + + .. attribute:: absolute_path + + A `collections.deque` containing the path to the + offending element within the instance. The absolute path + is always relative to the *original* instance that was + validated (i.e. the one passed into a validation method, *not* + `instance`\). The deque can be empty if the error happened + at the root of the instance. + + .. attribute:: json_path + + A `JSON path <https://goessner.net/articles/JsonPath/index.html>`_ + to the offending element within the instance. + + .. attribute:: path + + Same as `relative_path`. + + .. attribute:: instance + + The instance that was being validated. This will differ from + the instance originally passed into ``validate`` if the + validator object was in the process of validating a (possibly + nested) element within the top-level instance. The path within + the top-level instance (i.e. `ValidationError.path`) could + be used to find this object, but it is provided for convenience. + + .. attribute:: context + + If the error was caused by errors in subschemas, the list of errors + from the subschemas will be available on this property. The + `schema_path` and `path` of these errors will be relative + to the parent error. + + .. attribute:: cause + + If the error was caused by a *non*-validation error, the + exception object will be here. Currently this is only used + for the exception raised by a failed format checker in + `jsonschema.FormatChecker.check`. + + .. attribute:: parent + + A validation error which this error is the `context` of. + ``None`` if there wasn't one. + + +In case an invalid schema itself is encountered, a `SchemaError` is +raised. + +.. autoexception:: SchemaError + :noindex: + + The same attributes are present as for `ValidationError`\s. + + +These attributes can be clarified with a short example: + +.. testcode:: + + schema = { + "items": { + "anyOf": [ + {"type": "string", "maxLength": 2}, + {"type": "integer", "minimum": 5} + ] + } + } + instance = [{}, 3, "foo"] + v = Draft202012Validator(schema) + errors = sorted(v.iter_errors(instance), key=lambda e: e.path) + +The error messages in this situation are not very helpful on their own. + +.. testcode:: + + for error in errors: + print(error.message) + +outputs: + +.. testoutput:: + + {} is not valid under any of the given schemas + 3 is not valid under any of the given schemas + 'foo' is not valid under any of the given schemas + +If we look at `ValidationError.path` on each of the errors, we can find +out which elements in the instance correspond to each of the errors. In +this example, `ValidationError.path` will have only one element, which +will be the index in our list. + +.. testcode:: + + for error in errors: + print(list(error.path)) + +.. testoutput:: + + [0] + [1] + [2] + +Since our schema contained nested subschemas, it can be helpful to look at +the specific part of the instance and subschema that caused each of the errors. +This can be seen with the `ValidationError.instance` and +`ValidationError.schema` attributes. + +With keywords like :kw:`anyOf`, the `ValidationError.context` +attribute can be used to see the sub-errors which caused the failure. Since +these errors actually came from two separate subschemas, it can be helpful to +look at the `ValidationError.schema_path` attribute as well to see where +exactly in the schema each of these errors come from. In the case of sub-errors +from the `ValidationError.context` attribute, this path will be relative +to the `ValidationError.schema_path` of the parent error. + +.. testcode:: + + for error in errors: + for suberror in sorted(error.context, key=lambda e: e.schema_path): + print(list(suberror.schema_path), suberror.message, sep=", ") + +.. testoutput:: + + [0, 'type'], {} is not of type 'string' + [1, 'type'], {} is not of type 'integer' + [0, 'type'], 3 is not of type 'string' + [1, 'minimum'], 3 is less than the minimum of 5 + [0, 'maxLength'], 'foo' is too long + [1, 'type'], 'foo' is not of type 'integer' + +The string representation of an error combines some of these attributes for +easier debugging. + +.. testcode:: + + print(errors[1]) + +.. testoutput:: + + 3 is not valid under any of the given schemas + + Failed validating 'anyOf' in schema['items']: + {'anyOf': [{'maxLength': 2, 'type': 'string'}, + {'minimum': 5, 'type': 'integer'}]} + + On instance[1]: + 3 + + +ErrorTrees +---------- + +If you want to programmatically query which validation keywords +failed when validating a given instance, you may want to do so using +`jsonschema.exceptions.ErrorTree` objects. + +.. autoclass:: jsonschema.exceptions.ErrorTree + :noindex: + :members: + :special-members: + :exclude-members: __dict__,__weakref__ + + .. attribute:: errors + + The mapping of validator keywords to the error objects (usually + `jsonschema.exceptions.ValidationError`\s) at this level + of the tree. + +Consider the following example: + +.. testcode:: + + schema = { + "type" : "array", + "items" : {"type" : "number", "enum" : [1, 2, 3]}, + "minItems" : 3, + } + instance = ["spam", 2] + +For clarity's sake, the given instance has three errors under this schema: + +.. testcode:: + + v = Draft202012Validator(schema) + for error in sorted(v.iter_errors(["spam", 2]), key=str): + print(error.message) + +.. testoutput:: + + 'spam' is not of type 'number' + 'spam' is not one of [1, 2, 3] + ['spam', 2] is too short + +Let's construct an `jsonschema.exceptions.ErrorTree` so that we +can query the errors a bit more easily than by just iterating over the +error objects. + +.. testcode:: + + from jsonschema.exceptions import ErrorTree + tree = ErrorTree(v.iter_errors(instance)) + +As you can see, `jsonschema.exceptions.ErrorTree` takes an +iterable of `ValidationError`\s when constructing a tree so +you can directly pass it the return value of a validator object's +`jsonschema.protocols.Validator.iter_errors` method. + +`ErrorTree`\s support a number of useful operations. The first one we +might want to perform is to check whether a given element in our instance +failed validation. We do so using the :keyword:`in` operator: + +.. doctest:: + + >>> 0 in tree + True + + >>> 1 in tree + False + +The interpretation here is that the 0th index into the instance (``"spam"``) +did have an error (in fact it had 2), while the 1th index (``2``) did not (i.e. +it was valid). + +If we want to see which errors a child had, we index into the tree and look at +the `ErrorTree.errors` attribute. + +.. doctest:: + + >>> sorted(tree[0].errors) + ['enum', 'type'] + +Here we see that the :kw:`enum` and :kw:`type` keywords failed for +index ``0``. In fact `ErrorTree.errors` is a dict, whose values are the +`ValidationError`\s, so we can get at those directly if we want them. + +.. doctest:: + + >>> print(tree[0].errors["type"].message) + 'spam' is not of type 'number' + +Of course this means that if we want to know if a given validation +keyword failed for a given index, we check for its presence in +`ErrorTree.errors`: + +.. doctest:: + + >>> "enum" in tree[0].errors + True + + >>> "minimum" in tree[0].errors + False + +Finally, if you were paying close enough attention, you'll notice that +we haven't seen our :kw:`minItems` error appear anywhere yet. This is +because :kw:`minItems` is an error that applies globally to the instance +itself. So it appears in the root node of the tree. + +.. doctest:: + + >>> "minItems" in tree.errors + True + +That's all you need to know to use error trees. + +To summarize, each tree contains child trees that can be accessed by +indexing the tree to get the corresponding child tree for a given +index into the instance. Each tree and child has a `ErrorTree.errors` +attribute, a dict, that maps the failed validation keyword to the +corresponding validation error. + + +best_match and relevance +------------------------ + +The `best_match` function is a simple but useful function for attempting +to guess the most relevant error in a given bunch. + +.. doctest:: + + >>> from jsonschema import Draft202012Validator + >>> from jsonschema.exceptions import best_match + + >>> schema = { + ... "type": "array", + ... "minItems": 3, + ... } + >>> print(best_match(Draft202012Validator(schema).iter_errors(11)).message) + 11 is not of type 'array' + + +.. autofunction:: best_match + :noindex: + + +.. function:: relevance(validation_error) + :noindex: + + A key function that sorts errors based on heuristic relevance. + + If you want to sort a bunch of errors entirely, you can use + this function to do so. Using this function as a key to e.g. + `sorted` or `max` will cause more relevant errors to be + considered greater than less relevant ones. + + Within the different validation keywords that can fail, this + function considers :kw:`anyOf` and :kw:`oneOf` to be *weak* + validation errors, and will sort them lower than other errors at the + same level in the instance. + + If you want to change the set of weak [or strong] validation + keywords you can create a custom version of this function with + `by_relevance` and provide a different set of each. + +.. doctest:: + + >>> schema = { + ... "properties": { + ... "name": {"type": "string"}, + ... "phones": { + ... "properties": { + ... "home": {"type": "string"} + ... }, + ... }, + ... }, + ... } + >>> instance = {"name": 123, "phones": {"home": [123]}} + >>> errors = Draft202012Validator(schema).iter_errors(instance) + >>> [ + ... e.path[-1] + ... for e in sorted(errors, key=exceptions.relevance) + ... ] + ['home', 'name'] + + +.. autofunction:: by_relevance + :noindex: diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..5ae3e62 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,263 @@ +========================== +Frequently Asked Questions +========================== + +My schema specifies format validation. Why do invalid instances seem valid? +--------------------------------------------------------------------------- + +The :kw:`format` keyword can be a bit of a stumbling block for new +users working with JSON Schema. + +In a schema such as: + +.. code-block:: json + + {"type": "string", "format": "date"} + +JSON Schema specifications have historically differentiated between the +:kw:`format` keyword and other keywords. In particular, the +:kw:`format` keyword was specified to be *informational* as much +as it may be used for validation. + +In other words, for many use cases, schema authors may wish to use +values for the :kw:`format` keyword but have no expectation +they be validated alongside other required assertions in a schema. + +Of course this does not represent all or even most use cases -- many +schema authors *do* wish to assert that instances conform fully, even to +the specific format mentioned. + +In drafts prior to ``draft2019-09``, the decision on whether to +automatically enable :kw:`format` validation was left up to +validation implementations such as this one. + +This library made the choice to leave it off by default, for two reasons: + + * for forward compatibility and implementation complexity reasons + -- if :kw:`format` validation were on by default, and a + future draft of JSON Schema introduced a hard-to-implement format, + either the implementation of that format would block releases of + this library until it were implemented, or the behavior surrounding + :kw:`format` would need to be even more complex than simply + defaulting to be on. It therefore was safer to start with it off, + and defend against the expectation that a given format would always + automatically work. + + * given that a common use of JSON Schema is for portability across + languages (and therefore implementations of JSON Schema), so that + users be aware of this point itself regarding :kw:`format` + validation, and therefore remember to check any *other* + implementations they were using to ensure they too were explicitly + enabled for :kw:`format` validation. + +As of ``draft2019-09`` however, the opt-out by default behavior +mentioned here is now *required* for all validators. + +Difficult as this may sound for new users, at this point it at least +means they should expect the same behavior that has always been +implemented here, across any other implementation they encounter. + +.. seealso:: + + `Draft 2019-09's release notes on format <https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary>`_ + + for upstream details on the behavior of format and how it has changed + in ``draft2019-09`` + + `validating formats` + + for details on how to enable format validation + + `jsonschema.FormatChecker` + + the object which implements format validation + + +Can jsonschema be used to validate YAML, TOML, etc.? +---------------------------------------------------- + +Like most JSON Schema implementations, `jsonschema` doesn't actually deal directly with JSON at all (other than in relation to the :kw:`$ref` keyword, elaborated on below). + +In other words as far as this library is concerned, schemas and instances are simply runtime Python objects. +The JSON object ``{}`` is simply the Python `dict` ``{}``, and a JSON Schema like ``{"type": "object", {"properties": {}}}`` is really an assertion about particular Python objects and their keys. + +.. note:: + + The :kw:`$ref` keyword is a single notable exception. + + Specifically, in the case where `jsonschema` is asked to resolve a remote reference, it has no choice but to assume that the remote reference is serialized as JSON, and to deserialize it using the `json` module. + + One cannot today therefore reference some remote piece of YAML and have it deserialized into Python objects by this library without doing some additional work. + See `Resolving References to Schemas Written in YAML <referencing:Resolving References to Schemas Written in YAML>` for details. + +In practice what this means for JSON-like formats like YAML and TOML is that indeed one can generally schematize and then validate them exactly as if they were JSON by simply first deserializing them using libraries like ``PyYAML`` or the like, and passing the resulting Python objects into functions within this library. + +Beware however that there are cases where the behavior of the JSON Schema specification itself is only well-defined within the data model of JSON itself, and therefore only for Python objects that could have "in theory" come from JSON. +As an example, JSON supports only string-valued keys, whereas YAML supports additional types. +The JSON Schema specification does not deal with how to apply the :kw:`patternProperties` keyword to non-string properties. +The behavior of this library is therefore similarly not defined when presented with Python objects of this form, which could never have come from JSON. +In such cases one is recommended to first pre-process the data such that the resulting behavior is well-defined. +In the previous example, if the desired behavior is to transparently coerce numeric properties to strings, as Javascript might, then do the conversion explicitly before passing data to this library. + + +Why doesn't my schema's default property set the default on my instance? +------------------------------------------------------------------------ + +The basic answer is that the specification does not require that +:kw:`default` actually do anything. + +For an inkling as to *why* it doesn't actually do anything, consider +that none of the other keywords modify the instance either. More +importantly, having :kw:`default` modify the instance can produce +quite peculiar things. It's perfectly valid (and perhaps even useful) +to have a default that is not valid under the schema it lives in! So an +instance modified by the default would pass validation the first time, +but fail the second! + +Still, filling in defaults is a thing that is useful. `jsonschema` +allows you to `define your own validator classes and callables +<creating>`, so you can easily create an `jsonschema.protocols.Validator` +that does do default setting. Here's some code to get you started. (In +this code, we add the default properties to each object *before* the +properties are validated, so the default values themselves will need to +be valid under the schema.) + + .. testcode:: + + from jsonschema import Draft202012Validator, validators + + + def extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + for property, subschema in properties.items(): + if "default" in subschema: + instance.setdefault(property, subschema["default"]) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {"properties" : set_defaults}, + ) + + + DefaultValidatingValidator = extend_with_default(Draft202012Validator) + + + # Example usage: + obj = {} + schema = {'properties': {'foo': {'default': 'bar'}}} + # Note jsonschema.validate(obj, schema, cls=DefaultValidatingValidator) + # will not work because the metaschema contains `default` keywords. + DefaultValidatingValidator(schema).validate(obj) + assert obj == {'foo': 'bar'} + + +See the above-linked document for more info on how this works, +but basically, it just extends the :kw:`properties` keyword on a +`jsonschema.validators.Draft202012Validator` to then go ahead and update +all the defaults. + +.. note:: + + If you're interested in a more interesting solution to a larger + class of these types of transformations, keep an eye on `Seep + <https://github.com/Julian/Seep>`_, which is an experimental + data transformation and extraction library written on top of + `jsonschema`. + + +.. hint:: + + The above code can provide default values for an entire object and + all of its properties, but only if your schema provides a default + value for the object itself, like so: + + .. testcode:: + + schema = { + "type": "object", + "properties": { + "outer-object": { + "type": "object", + "properties" : { + "inner-object": { + "type": "string", + "default": "INNER-DEFAULT" + } + }, + "default": {} # <-- MUST PROVIDE DEFAULT OBJECT + } + } + } + + obj = {} + DefaultValidatingValidator(schema).validate(obj) + assert obj == {'outer-object': {'inner-object': 'INNER-DEFAULT'}} + + ...but if you don't provide a default value for your object, then + it won't be instantiated at all, much less populated with default + properties. + + .. testcode:: + + del schema["properties"]["outer-object"]["default"] + obj2 = {} + DefaultValidatingValidator(schema).validate(obj2) + assert obj2 == {} # whoops + + +How do jsonschema version numbers work? +--------------------------------------- + +``jsonschema`` tries to follow the `Semantic Versioning +<https://semver.org/>`_ specification. + +This means broadly that no backwards-incompatible changes should be made +in minor releases (and certainly not in dot releases). + +The full picture requires defining what constitutes a +backwards-incompatible change. + +The following are simple examples of things considered public API, +and therefore should *not* be changed without bumping a major version +number: + + * module names and contents, when not marked private by Python + convention (a single leading underscore) + + * function and object signature (parameter order and name) + +The following are *not* considered public API and may change without +notice: + + * the exact wording and contents of error messages; typical reasons + to rely on this seem to involve downstream tests in packages using + `jsonschema`. These use cases are encouraged to use the extensive + introspection provided in `jsonschema.exceptions.ValidationError`\s + instead to make meaningful assertions about what failed rather than + relying on *how* what failed is explained to a human. + + * the order in which validation errors are returned or raised + + * the contents of the ``jsonschema.tests`` package + + * the contents of the ``jsonschema.benchmarks`` package + + * the specific non-zero error codes presented by the command line + interface + + * the exact representation of errors presented by the command line + interface, other than that errors represented by the plain outputter + will be reported one per line + + * anything marked private + +With the exception of the last two of those, flippant changes are +avoided, but changes can and will be made if there is improvement to be +had. Feel free to open an issue ticket if there is a specific issue or +question worth raising. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..949ab44 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +.. module:: jsonschema + :noindex: + +.. include:: ../README.rst + + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + validate + errors + referencing + creating + faq + api/index + + +Indices and tables +================== + +* `genindex` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fcb914f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^<target^>` where ^<target^> is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\jsonschema.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\jsonschema.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/referencing.rst b/docs/referencing.rst new file mode 100644 index 0000000..e3f0e7f --- /dev/null +++ b/docs/referencing.rst @@ -0,0 +1,375 @@ +========================= +JSON (Schema) Referencing +========================= + +The JSON Schema :kw:`$ref` and :kw:`$dynamicRef` keywords allow schema authors to combine multiple schemas (or subschemas) together for reuse or deduplication. + +The `referencing <referencing:index>` library was written in order to provide a simple, well-behaved and well-tested implementation of this kind of reference resolution [1]_. +It has its `own documentation which is worth reviewing <referencing:intro>`, but this page serves as an introduction which is tailored specifically to JSON Schema, and even more specifically to how to configure `referencing <referencing:index>` for use with `Validator` objects in order to customize the behavior of the :kw:`$ref` keyword and friends in your schemas. + +Configuring `jsonschema` for custom referencing behavior is essentially a two step process: + + * Create a `referencing.Registry` object that behaves the way you wish + + * Pass the `referencing.Registry` to your `Validator` when instantiating it + +The examples below essentially follow these two steps. + +.. [1] One that in fact is independent of this `jsonschema` library itself, and may some day be used by other tools or implementations. + + +Introduction to the `referencing <referencing:index>` API +--------------------------------------------------------- + +There are 3 main objects to be aware of in the `referencing` API: + + * `referencing.Registry`, which represents a specific immutable set of JSON Schemas (either in-memory or retrievable) + * `referencing.Specification`, which represents a specific *version* of the JSON Schema specification, which can have differing referencing behavior. + JSON Schema-specific specifications live in the `referencing.jsonschema` module and are named like `referencing.jsonschema.DRAFT202012`. + * `referencing.Resource`, which represents a specific JSON Schema (often a Python `dict`) *along* with a specific `referencing.Specification` it is to be interpreted under. + +As a concrete example, the simple schema ``{"type": "integer"}`` may be interpreted as a schema under either Draft 2020-12 or Draft 4 of the JSON Schema specification (amongst others); in draft 2020-12, the float ``2.0`` must be considered an integer, whereas in draft 4, it potentially is not. +If you mean the former (i.e. to associate this schema with draft 2020-12), you'd use ``referencing.Resource(contents={"type": "integer"}, specification=referencing.jsonschema.DRAFT202012)``, whereas for the latter you'd use `referencing.jsonschema.DRAFT4`. + +.. seealso:: the JSON Schema :kw:`$schema` keyword + + Which should generally be used to remove all ambiguity and identify *internally* to the schema what version it is written for. + +A schema may be identified via one or more URIs, either because they contain an :kw:`$id` keyword (in suitable versions of the JSON Schema specification) which indicates their canonical URI, or simply because you wish to externally associate a URI with the schema, regardless of whether it contains an ``$id`` keyword. +You could add the aforementioned simple schema to a `referencing.Registry` by creating an empty registry and then identifying it via some URI: + +.. testcode:: + + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT202012 + schema = Resource(contents={"type": "integer"}, specification=DRAFT202012) + registry = Registry().with_resource(uri="http://example.com/my/schema", resource=schema) + print(registry) + +.. testoutput:: + + <Registry (1 uncrawled resource)> + +.. note:: + + `referencing.Registry` is an entirely immutable object. + All of its methods which add schemas (resources) to itself return *new* registry objects containing the added schemas. + +You could also confirm your schema is in the registry if you'd like, via `referencing.Registry.contents`, which will show you the contents of a resource at a given URI: + +.. testcode:: + + print(registry.contents("http://example.com/my/schema")) + +.. testoutput:: + + {'type': 'integer'} + +For further details, see the `referencing documentation <referencing:intro>`. + +Common Scenarios +---------------- + +.. _in-memory-schemas: + +Making Additional In-Memory Schemas Available +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The most common scenario one is likely to encounter is the desire to include a small number of additional in-memory schemas, making them available for use during validation. + +For instance, imagine the below schema for non-negative integers: + +.. code:: json + + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "minimum": 0 + } + +We may wish to have other schemas we write be able to make use of this schema, and refer to it as ``http://example.com/nonneg-int-schema`` and/or as ``urn:nonneg-integer-schema``. + +To do so we make use of APIs from the referencing library to create a `referencing.Registry` which maps the URIs above to this schema: + +.. code:: python + + from referencing import Registry, Resource + schema = Resource.from_contents( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "minimum": 0, + }, + ) + registry = Registry().with_resources( + [ + ("http://example.com/nonneg-int-schema", schema), + ("urn:nonneg-integer-schema", schema), + ], + ) + +What's above is likely mostly self-explanatory, other than the presence of the `referencing.Resource.from_contents` function. +Its purpose is to convert a piece of "opaque" JSON (or really a Python `dict` containing deserialized JSON) into an object which indicates what *version* of JSON Schema the schema is meant to be interpreted under. +Calling it will inspect a :kw:`$schema` keyword present in the given schema and use that to associate the JSON with an appropriate `specification <referencing.Specification>`. +If your schemas do not contain ``$schema`` dialect identifiers, and you intend for them to be interpreted always under a specific dialect -- say Draft 2020-12 of JSON Schema -- you may instead use e.g.: + +.. code:: python + + from referencing import Registry, Resource + from referencing.jsonschema import DRAFT2020212 + schema = DRAFT202012.create_resource({"type": "integer", "minimum": 0}) + registry = Registry().with_resources( + [ + ("http://example.com/nonneg-int-schema", schema), + ("urn:nonneg-integer-schema", schema), + ], + ) + +which has the same functional effect. + +You can now pass this registry to your `Validator`, which allows a schema passed to it to make use of the aforementioned URIs to refer to our non-negative integer schema. +Here for instance is an example which validates that instances are JSON objects with non-negative integral values: + +.. code:: python + + from jsonschema import Draft202012Validator + validator = Draft202012Validator( + { + "type": "object", + "additionalProperties": {"$ref": "urn:nonneg-integer-schema"}, + }, + registry=registry, # the critical argument, our registry from above + ) + validator.validate({"foo": 37}) + validator.validate({"foo": -37}) # Uh oh! + +.. _ref-filesystem: + +Resolving References from the File System +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another common request from schema authors is to be able to map URIs to the file system, perhaps while developing a set of schemas in different local files. +The referencing library supports doing so dynamically by configuring a callable which can be used to retrieve any schema which is *not* already pre-loaded in the manner described `above <in-memory-schemas>`. + +Here we resolve any schema beginning with ``http://localhost`` to a directory ``/tmp/schemas`` on the local filesystem (note of course that this will not work if run directly unless you have populated that directory with some schemas): + +.. code:: python + + from pathlib import Path + import json + + from referencing import Registry, Resource + from referencing.exceptions import NoSuchResource + + SCHEMAS = Path("/tmp/schemas") + + def retrieve_from_filesystem(uri: str): + if not uri.startswith("http://localhost/"): + raise NoSuchResource(ref=uri) + path = SCHEMAS / Path(uri.removeprefix("http://localhost/")) + contents = json.loads(path.read_text()) + return Resource.from_contents(contents) + + registry = Registry(retrieve=retrieve_from_filesystem) + +Such a registry can then be used with `Validator` objects in the same way shown above, and any such references to URIs which are not already in-memory will be retrieved from the configured directory. + +We can mix the two examples above if we wish for some in-memory schemas to be available in addition to the filesystem schemas, e.g.: + +.. code:: python + + from referencing.jsonschema import DRAFT7 + registry = Registry(retrieve=retrieve_from_filesystem).with_resource( + "urn:non-empty-array", DRAFT7.create_resource({"type": "array", "minItems": 1}), + ) + +where we've made use of the similar `referencing.Registry.with_resource` function to add a single additional resource. + +Resolving References to Schemas Written in YAML +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generalizing slightly, the retrieval function provided need not even assume that it is retrieving JSON. +As long as you deserialize what you have retrieved into Python objects, you may equally be retrieving references to YAML documents or any other format. + +Here for instance we retrieve YAML documents in a way similar to the `above <ref-filesystem>` using PyYAML: + +.. code:: python + + from pathlib import Path + import yaml + + from referencing import Registry, Resource + from referencing.exceptions import NoSuchResource + + SCHEMAS = Path("/tmp/yaml-schemas") + + def retrieve_yaml(uri: str): + if not uri.startswith("http://localhost/"): + raise NoSuchResource(ref=uri) + path = SCHEMAS / Path(uri.removeprefix("http://localhost/")) + contents = yaml.safe_load(path.read_text()) + return Resource.from_contents(contents) + + registry = Registry(retrieve=retrieve_yaml) + +.. note:: + + Not all YAML fits within the JSON data model. + + JSON Schema is defined specifically for JSON, and has well-defined behavior strictly for Python objects which could have possibly existed as JSON. + + If you stick to the subset of YAML for which this is the case then you shouldn't have issue, but if you pass schemas (or instances) around whose structure could never have possibly existed as JSON (e.g. a mapping whose keys are not strings), all bets are off. + +One could similarly imagine a retrieval function which switches on whether to call ``yaml.safe_load`` or ``json.loads`` by file extension (or some more reliable mechanism) and thereby support retrieving references of various different file formats. + +.. _http: + +Automatically Retrieving Resources Over HTTP +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the general case, the JSON Schema specifications tend to `discourage <https://json-schema.org/draft/2020-12/json-schema-core.html#name-loading-a-referenced-schema>`_ implementations (like this one) from automatically retrieving references over the network, or even assuming such a thing is feasible (as schemas may be identified by URIs which are strictly identifiers, and not necessarily downloadable from the URI even when such a thing is sensical). + +However, if you as a schema author are in a situation where you indeed do wish to do so for convenience (and understand the implications of doing so), you may do so by making use of the ``retrieve`` argument to `referencing.Registry`. + +Here is how one would configure a registry to automatically retrieve schemas from the `JSON Schema Store <https://www.schemastore.org>`_ on the fly using the `httpx <https://www.python-httpx.org/>`_: + +.. code:: python + + from referencing import Registry, Resource + import httpx + + def retrieve_via_httpx(uri: str): + response = httpx.get(uri) + return Resource.from_contents(response.json()) + + registry = Registry(retrieve=retrieve_via_httpx) + +Given such a registry, we can now, for instance, validate instances against schemas from the schema store by passing the ``registry`` we configured to our `Validator` as in previous examples: + +.. code:: python + + from jsonschema import Draft202012Validator + Draft202012Validator( + {"$ref": "https://json.schemastore.org/pyproject.json"}, + registry=registry, + ).validate({"project": {"name": 12}}) + +which should in this case indicate the example data is invalid: + +.. code:: python + + Traceback (most recent call last): + File "example.py", line 14, in <module> + ).validate({"project": {"name": 12}}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "jsonschema/validators.py", line 345, in validate + raise error + jsonschema.exceptions.ValidationError: 12 is not of type 'string' + + Failed validating 'type' in schema['properties']['project']['properties']['name']: + {'pattern': '^([a-zA-Z\\d]|[a-zA-Z\\d][\\w.-]*[a-zA-Z\\d])$', + 'title': 'Project name', + 'type': 'string'} + + On instance['project']['name']: + 12 + +Retrieving resources from a SQLite database or some other network-accessible resource should be more or less similar, replacing the HTTP client with one for your database of course. + +.. warning:: + + Be sure you understand the security implications of the reference resolution you configure. + And if you accept untrusted schemas, doubly sure! + + You wouldn't want a user causing your machine to go off and retrieve giant files off the network by passing it a ``$ref`` to some huge blob, or exploiting similar vulnerabilities in your setup. + + +Migrating From ``RefResolver`` +------------------------------ + +Older versions of `jsonschema` used a different object -- `_RefResolver` -- for reference resolution, which you a schema author may already be configuring for your own use. + +`_RefResolver` is now fully deprecated and replaced by the use of `referencing.Registry` as shown in examples above. + +If you are not already constructing your own `_RefResolver`, this change should be transparent to you (or even recognizably improved, as the point of the migration was to improve the quality of the referencing implementation and enable some new functionality). + +.. table:: Rough equivalence between `_RefResolver` and `referencing.Registry` APIs + :widths: auto + + =========================================================== ===================================================================================================================== + Old API New API + =========================================================== ===================================================================================================================== + ``RefResolver.from_schema({"$id": "urn:example:foo", ...}`` ``Registry().with_resource(uri="urn:example:foo", resource=Resource.from_contents({"$id": "urn:example:foo", ...}))`` + Overriding ``RefResolver.resolve_from_url`` Passing a callable to `referencing.Registry`\ 's ``retrieve`` argument + ``DraftNValidator(..., resolver=_RefResolver(...))`` `` DraftNValidator(..., registry=Registry().with_resources(...))`` + =========================================================== ===================================================================================================================== + + +Here are some more specifics on how to migrate to the newer APIs: + +The ``store`` argument +~~~~~~~~~~~~~~~~~~~~~~ + +`_RefResolver`\ 's ``store`` argument was essentially the equivalent of `referencing.Registry`\ 's in-memory schema storage. + +If you currently pass a set of schemas via e.g.: + +.. code:: python + + from jsonschema import Draft202012Validator, RefResolver + resolver = RefResolver.from_schema( + schema={"title": "my schema"}, + store={"http://example.com": {"type": "integer"}}, + ) + validator = Draft202012Validator( + {"$ref": "http://example.com"}, + resolver=resolver, + ) + validator.validate("foo") + +you should be able to simply move to something like: + +.. code:: python + + from referencing import Registry + from referencing.jsonschema import DRAFT202012 + + from jsonschema import Draft202012Validator + + registry = Registry().with_resource( + "http://example.com", + DRAFT202012.create_resource({"type": "integer"}), + ) + validator = Draft202012Validator( + {"$ref": "http://example.com"}, + registry=registry, + ) + validator.validate("foo") + +Handlers +~~~~~~~~ + +The ``handlers`` functionality from `_RefResolver` was a way to support additional HTTP schemes for schema retrieval. + +Here you should move to a custom ``retrieve`` function which does whatever you'd like. +E.g. in pseudocode: + +.. code:: python + + from urllib.parse import urlsplit + + def retrieve(uri: str): + parsed = urlsplit(uri) + if parsed.scheme == "file": + ... + elif parsed.scheme == "custom": + ... + + registry = Registry(retrieve=retrieve) + + +Other Key Functional Differences +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whilst `_RefResolver` *did* automatically retrieve remote references (against the recommendation of the spec, and in a way which therefore could lead to questionable security concerns when combined with untrusted schemas), `referencing.Registry` does *not* do so. +If you rely on this behavior, you should follow the `above example of retrieving resources over HTTP <http>`. diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 0000000..0a686cf --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,10 @@ +file:.#egg=jsonschema +furo +lxml +sphinx +sphinx-autoapi +sphinx-autodoc-typehints +sphinx-copybutton +sphinx-json-schema-spec +sphinxcontrib-spelling +sphinxext-opengraph diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..01f2edf --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,137 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --resolver=backtracking docs/requirements.in +# +alabaster==0.7.13 + # via sphinx +astroid==2.15.0 + # via sphinx-autoapi +attrs==22.2.0 + # via + # jsonschema + # referencing +babel==2.12.1 + # via sphinx +beautifulsoup4==4.12.0 + # via furo +certifi==2022.12.7 + # via requests +charset-normalizer==3.1.0 + # via requests +contourpy==1.0.7 + # via matplotlib +cycler==0.11.0 + # via matplotlib +docutils==0.19 + # via sphinx +fonttools==4.39.2 + # via matplotlib +furo==2022.12.7 + # via -r docs/requirements.in +idna==3.4 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via + # sphinx + # sphinx-autoapi +file:.#egg=jsonschema + # via -r docs/requirements.in +jsonschema-specifications==2023.3.6 + # via jsonschema +kiwisolver==1.4.4 + # via matplotlib +lazy-object-proxy==1.9.0 + # via astroid +lxml==4.9.2 + # via + # -r docs/requirements.in + # sphinx-json-schema-spec +markupsafe==2.1.2 + # via jinja2 +matplotlib==3.7.1 + # via sphinxext-opengraph +numpy==1.24.2 + # via + # contourpy + # matplotlib +packaging==23.0 + # via + # matplotlib + # sphinx +pillow==9.4.0 + # via matplotlib +pyenchant==3.2.2 + # via sphinxcontrib-spelling +pygments==2.14.0 + # via + # furo + # sphinx +pyparsing==3.0.9 + # via matplotlib +python-dateutil==2.8.2 + # via matplotlib +pyyaml==6.0 + # via sphinx-autoapi +referencing==0.25.3 + # via + # jsonschema + # jsonschema-specifications +requests==2.28.2 + # via sphinx +rpds-py==0.7.1 + # via + # jsonschema + # referencing +six==1.16.0 + # via python-dateutil +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.4 + # via beautifulsoup4 +sphinx==6.1.3 + # via + # -r docs/requirements.in + # furo + # sphinx-autoapi + # sphinx-autodoc-typehints + # sphinx-basic-ng + # sphinx-copybutton + # sphinx-json-schema-spec + # sphinxcontrib-spelling + # sphinxext-opengraph +sphinx-autoapi==2.0.1 + # via -r docs/requirements.in +sphinx-autodoc-typehints==1.22 + # via -r docs/requirements.in +sphinx-basic-ng==1.0.0b1 + # via furo +sphinx-copybutton==0.5.1 + # via -r docs/requirements.in +sphinx-json-schema-spec==2023.2.4 + # via -r docs/requirements.in +sphinxcontrib-applehelp==1.0.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +sphinxcontrib-spelling==8.0.0 + # via -r docs/requirements.in +sphinxext-opengraph==0.8.1 + # via -r docs/requirements.in +unidecode==1.3.6 + # via sphinx-autoapi +urllib3==1.26.15 + # via requests +wrapt==1.15.0 + # via astroid diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt new file mode 100644 index 0000000..640d56f --- /dev/null +++ b/docs/spelling-wordlist.txt @@ -0,0 +1,59 @@ +# this appears to be misinterpreting Napoleon types as prose, sigh... +Validator +TypeChecker +UnknownType +ValidationError + +# 0th, sigh... +th +amongst +callables +# non-codeblocked cls from autoapi +cls +deque +deduplication +dereferences +deserialize +deserialized +deserializing +filesystem +hostname +implementers +indices +# ipv4/6, sigh... +ipv +iterable +iteratively +Javascript +jsonschema +majorly +metaschema +online +outputter +pre +programmatically +pseudocode +recurses +regex +repr +runtime +sensical +subclassing +submodule +submodules +subschema +subschemas +subscopes +untrusted +uri +validator +validators +versioned +schemas + +Zac +HD + +Berman +Libera +GPL diff --git a/docs/validate.rst b/docs/validate.rst new file mode 100644 index 0000000..71ec19d --- /dev/null +++ b/docs/validate.rst @@ -0,0 +1,306 @@ +================= +Schema Validation +================= + + +.. currentmodule:: jsonschema + +.. tip:: + + Most of the documentation for this package assumes you're familiar with the fundamentals of writing JSON schemas themselves, and focuses on how this library helps you validate with them in Python. + + If you aren't already comfortable with writing schemas and need an introduction which teaches about JSON Schema the specification, you may find `Understanding JSON Schema <ujs:basics>` to be a good read! + + +The Basics +---------- + +The simplest way to validate an instance under a given schema is to use the +`validate <jsonschema.validators.validate>` function. + +.. autofunction:: validate + :noindex: + +.. _validator-protocol: + +The Validator Protocol +---------------------- + +`jsonschema` defines a `protocol <typing.Protocol>` that all validator classes adhere to. + +.. hint:: + + If you are unfamiliar with protocols, either as a general notion or as specifically implemented by `typing.Protocol`, you can think of them as a set of attributes and methods that all objects satisfying the protocol have. + + Here, in the context of `jsonschema`, the `Validator.iter_errors` method can be called on `jsonschema.validators.Draft202012Validator`, or `jsonschema.validators.Draft7Validator`, or indeed any validator class, as all of them have it, along with all of the other methods described below. + +.. autoclass:: jsonschema.protocols.Validator + :noindex: + :members: + +All of the `versioned validators <versioned-validators>` that are included with `jsonschema` adhere to the protocol, and any `extensions of these validators <jsonschema.validators.extend>` will as well. +For more information on `creating <jsonschema.validators.create>` or `extending <jsonschema.validators.extend>` validators see `creating-validators`. + +Type Checking +------------- + +To handle JSON Schema's :kw:`type` keyword, a `Validator` uses +an associated `TypeChecker`. The type checker provides an immutable +mapping between names of types and functions that can test if an instance is +of that type. The defaults are suitable for most users - each of the +`versioned validators <versioned-validators>` that are included with +`jsonschema` have a `TypeChecker` that can correctly handle their respective +versions. + +.. seealso:: `validating-types` + + For an example of providing a custom type check. + +.. autoclass:: TypeChecker + :members: + :noindex: + +.. autoexception:: jsonschema.exceptions.UndefinedTypeCheck + :noindex: + + Raised when trying to remove a type check that is not known to this + TypeChecker, or when calling `jsonschema.TypeChecker.is_type` + directly. + +.. _validating-types: + +Validating With Additional Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Occasionally it can be useful to provide additional or alternate types when +validating JSON Schema's :kw:`type` keyword. + +`jsonschema` tries to strike a balance between performance in the common +case and generality. For instance, JSON Schema defines a ``number`` type, which +can be validated with a schema such as ``{"type" : "number"}``. By default, +this will accept instances of Python `numbers.Number`. This includes in +particular `int`\s and `float`\s, along with +`decimal.Decimal` objects, `complex` numbers etc. For +``integer`` and ``object``, however, rather than checking for +`numbers.Integral` and `collections.abc.Mapping`, +`jsonschema` simply checks for `int` and `dict`, since the +more general instance checks can introduce significant slowdown, especially +given how common validating these types are. + +If you *do* want the generality, or just want to add a few specific additional +types as being acceptable for a validator object, then you should update an +existing `jsonschema.TypeChecker` or create a new one. You may then create a new +`Validator` via `jsonschema.validators.extend`. + +.. testcode:: + + from jsonschema import validators + + class MyInteger: + pass + + def is_my_int(checker, instance): + return ( + Draft202012Validator.TYPE_CHECKER.is_type(instance, "number") or + isinstance(instance, MyInteger) + ) + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "number", is_my_int, + ) + + CustomValidator = validators.extend( + Draft202012Validator, + type_checker=type_checker, + ) + validator = CustomValidator(schema={"type" : "number"}) + + +.. autoexception:: jsonschema.exceptions.UnknownType + :noindex: + +.. _versioned-validators: + +Versioned Validators +-------------------- + +`jsonschema` ships with validator classes for various versions of the JSON Schema specification. +For details on the methods and attributes that each validator class provides see the `Validator` protocol, which each included validator class implements. + +Each of the below cover a specific release of the JSON Schema specification. + +.. autoclass:: Draft202012Validator + :noindex: + +.. autoclass:: Draft201909Validator + :noindex: + +.. autoclass:: Draft7Validator + :noindex: + +.. autoclass:: Draft6Validator + :noindex: + +.. autoclass:: Draft4Validator + :noindex: + +.. autoclass:: Draft3Validator + :noindex: + + +For example, if you wanted to validate a schema you created against the +Draft 2020-12 meta-schema, you could use: + +.. testcode:: + + from jsonschema import Draft202012Validator + + schema = { + "$schema": Draft202012Validator.META_SCHEMA["$id"], + + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + }, + "required": ["email"] + } + Draft202012Validator.check_schema(schema) + + +.. _validating formats: + +Validating Formats +------------------ + +JSON Schema defines the :kw:`format` keyword which can be used to check if primitive types (``string``\s, ``number``\s, ``boolean``\s) conform to well-defined formats. +By default, as per the specification, no validation is enforced. +Optionally however, validation can be enabled by hooking a `format-checking object <jsonschema.FormatChecker>` into a `Validator`. + +.. doctest:: + + >>> validate("127.0.0.1", {"format" : "ipv4"}) + >>> validate( + ... instance="-12", + ... schema={"format" : "ipv4"}, + ... format_checker=Draft202012Validator.FORMAT_CHECKER, + ... ) + Traceback (most recent call last): + ... + ValidationError: "-12" is not a "ipv4" + + +Some formats require additional dependencies to be installed. + +The easiest way to ensure you have what is needed is to install ``jsonschema`` using the ``format`` or ``format-nongpl`` extras. + +For example: + +.. code:: sh + + $ pip install jsonschema[format] + +Or if you want to avoid GPL dependencies, a second extra is available: + +.. code:: sh + + $ pip install jsonschema[format-nongpl] + +At the moment, it supports all the available checkers except for ``iri`` and ``iri-reference``. + +.. warning:: + + It is your own responsibility ultimately to ensure you are license-compliant, so you should be double checking your own dependencies if you rely on this extra. + +The more specific list of formats along with any additional dependencies they have is shown below. + +.. warning:: + + If a dependency is not installed when using a checker that requires it, validation will succeed without throwing an error, as also specified by the specification. + +========================= ==================== +Checker Notes +========================= ==================== +``color`` requires webcolors_ +``date`` +``date-time`` requires rfc3339-validator_ +``duration`` requires isoduration_ +``email`` +``hostname`` requires fqdn_ +``idn-hostname`` requires idna_ +``ipv4`` +``ipv6`` OS must have `socket.inet_pton` function +``iri`` requires rfc3987_ +``iri-reference`` requires rfc3987_ +``json-pointer`` requires jsonpointer_ +``regex`` +``relative-json-pointer`` requires jsonpointer_ +``time`` requires rfc3339-validator_ +``uri`` requires rfc3987_ or rfc3986-validator_ +``uri-reference`` requires rfc3987_ or rfc3986-validator_ +``uri-template`` requires uri-template_ +========================= ==================== + + +.. _fqdn: https://pypi.org/pypi/fqdn/ +.. _idna: https://pypi.org/pypi/idna/ +.. _isoduration: https://pypi.org/pypi/isoduration/ +.. _jsonpointer: https://pypi.org/pypi/jsonpointer/ +.. _rfc3339-validator: https://pypi.org/project/rfc3339-validator/ +.. _rfc3986-validator: https://pypi.org/project/rfc3986-validator/ +.. _rfc3987: https://pypi.org/pypi/rfc3987/ +.. _uri-template: https://pypi.org/pypi/uri-template/ +.. _webcolors: https://pypi.org/pypi/webcolors/ + +The supported mechanism for ensuring these dependencies are present is again as shown above, not by directly installing the packages. + +.. autoclass:: FormatChecker + :members: + :noindex: + :exclude-members: cls_checks + + .. attribute:: checkers + + A mapping of currently known formats to tuple of functions that validate them and errors that should be caught. + New checkers can be added and removed either per-instance or globally for all checkers using the `FormatChecker.checks` decorator. + + .. classmethod:: cls_checks(format, raises=()) + + Register a decorated function as *globally* validating a new format. + + Any instance created after this function is called will pick up the supplied checker. + + :argument str format: the format that the decorated function will check + :argument Exception raises: the exception(s) raised + by the decorated function when an invalid instance is + found. The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute + of the resulting validation error. + + .. deprecated:: v4.14.0 + + Use `FormatChecker.checks` on an instance instead. + +.. autoexception:: FormatError + :noindex: + :members: + + +Format-Specific Notes +~~~~~~~~~~~~~~~~~~~~~ + +regex +^^^^^ + +The JSON Schema specification `recommends (but does not require) <https://json-schema.org/draft/2020-12/json-schema-core.html#name-regular-expressions>`_ that implementations use ECMA 262 regular expressions. + +Given that there is no current library in Python capable of supporting the ECMA 262 dialect, the ``regex`` format will instead validate *Python* regular expressions, which are the ones used by this implementation for other keywords like :kw:`pattern` or :kw:`patternProperties`. + +email +^^^^^ + +Since in most cases "validating" an email address is an attempt instead to confirm that mail sent to it will deliver to a recipient, and that that recipient is the correct one the email is intended for, and since many valid email addresses are in many places incorrectly rejected, and many invalid email addresses are in many places incorrectly accepted, the ``email`` format keyword only provides a sanity check, not full :RFC:`5322` validation. + +The same applies to the ``idn-email`` format. + +If you indeed want a particular well-specified set of emails to be considered valid, you can use `FormatChecker.checks` to provide your specific definition. diff --git a/.editorconfig b/json/.editorconfig index 6db6a5b..6db6a5b 100644 --- a/.editorconfig +++ b/json/.editorconfig diff --git a/.github/CODEOWNERS b/json/.github/CODEOWNERS index 15f4a2d..15f4a2d 100644 --- a/.github/CODEOWNERS +++ b/json/.github/CODEOWNERS diff --git a/json/.github/workflows/ci.yml b/json/.github/workflows/ci.yml new file mode 100644 index 0000000..a826069 --- /dev/null +++ b/json/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: Test Suite Sanity Checking + +on: + push: + pull_request: + release: + types: [published] + schedule: + # Daily at 6:42, arbitrarily as a time that's possibly non-busy + - cron: '42 6 * * *' + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install tox + run: python -m pip install tox + - name: Run the sanity checks + run: python -m tox diff --git a/json/.gitignore b/json/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/json/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/CONTRIBUTING.md b/json/CONTRIBUTING.md index 8dab09e..8dab09e 100644 --- a/CONTRIBUTING.md +++ b/json/CONTRIBUTING.md diff --git a/README.md b/json/README.md index 36dda74..36dda74 100644 --- a/README.md +++ b/json/README.md diff --git a/bin/jsonschema_suite b/json/bin/jsonschema_suite index b4792a0..b4792a0 100755 --- a/bin/jsonschema_suite +++ b/json/bin/jsonschema_suite diff --git a/output-test-schema.json b/json/output-test-schema.json index 02c51ef..02c51ef 100644 --- a/output-test-schema.json +++ b/json/output-test-schema.json diff --git a/output-tests/README.md b/json/output-tests/README.md index 0cd7c87..0cd7c87 100644 --- a/output-tests/README.md +++ b/json/output-tests/README.md diff --git a/output-tests/draft-next/content/general.json b/json/output-tests/draft-next/content/general.json index 27082ed..27082ed 100644 --- a/output-tests/draft-next/content/general.json +++ b/json/output-tests/draft-next/content/general.json diff --git a/output-tests/draft-next/content/readOnly.json b/json/output-tests/draft-next/content/readOnly.json index d387d93..d387d93 100644 --- a/output-tests/draft-next/content/readOnly.json +++ b/json/output-tests/draft-next/content/readOnly.json diff --git a/output-tests/draft-next/content/type.json b/json/output-tests/draft-next/content/type.json index e17f1f5..e17f1f5 100644 --- a/output-tests/draft-next/content/type.json +++ b/json/output-tests/draft-next/content/type.json diff --git a/output-tests/draft-next/output-schema.json b/json/output-tests/draft-next/output-schema.json index 26286fa..26286fa 100644 --- a/output-tests/draft-next/output-schema.json +++ b/json/output-tests/draft-next/output-schema.json diff --git a/output-tests/draft2019-09/content/general.json b/json/output-tests/draft2019-09/content/general.json index 9194170..9194170 100644 --- a/output-tests/draft2019-09/content/general.json +++ b/json/output-tests/draft2019-09/content/general.json diff --git a/output-tests/draft2019-09/content/readOnly.json b/json/output-tests/draft2019-09/content/readOnly.json index 62db1a8..62db1a8 100644 --- a/output-tests/draft2019-09/content/readOnly.json +++ b/json/output-tests/draft2019-09/content/readOnly.json diff --git a/output-tests/draft2019-09/content/type.json b/json/output-tests/draft2019-09/content/type.json index cff77a7..cff77a7 100644 --- a/output-tests/draft2019-09/content/type.json +++ b/json/output-tests/draft2019-09/content/type.json diff --git a/output-tests/draft2019-09/output-schema.json b/json/output-tests/draft2019-09/output-schema.json index 0a65f20..0a65f20 100644 --- a/output-tests/draft2019-09/output-schema.json +++ b/json/output-tests/draft2019-09/output-schema.json diff --git a/output-tests/draft2020-12/content/general.json b/json/output-tests/draft2020-12/content/general.json index 1f2b370..1f2b370 100644 --- a/output-tests/draft2020-12/content/general.json +++ b/json/output-tests/draft2020-12/content/general.json diff --git a/output-tests/draft2020-12/content/readOnly.json b/json/output-tests/draft2020-12/content/readOnly.json index 9baf48d..9baf48d 100644 --- a/output-tests/draft2020-12/content/readOnly.json +++ b/json/output-tests/draft2020-12/content/readOnly.json diff --git a/output-tests/draft2020-12/content/type.json b/json/output-tests/draft2020-12/content/type.json index 710475b..710475b 100644 --- a/output-tests/draft2020-12/content/type.json +++ b/json/output-tests/draft2020-12/content/type.json diff --git a/output-tests/draft2020-12/output-schema.json b/json/output-tests/draft2020-12/output-schema.json index 1eef288..1eef288 100644 --- a/output-tests/draft2020-12/output-schema.json +++ b/json/output-tests/draft2020-12/output-schema.json diff --git a/package.json b/json/package.json index 75da9e2..75da9e2 100644 --- a/package.json +++ b/json/package.json diff --git a/remotes/baseUriChange/folderInteger.json b/json/remotes/baseUriChange/folderInteger.json index 8b50ea3..8b50ea3 100644 --- a/remotes/baseUriChange/folderInteger.json +++ b/json/remotes/baseUriChange/folderInteger.json diff --git a/remotes/baseUriChangeFolder/folderInteger.json b/json/remotes/baseUriChangeFolder/folderInteger.json index 8b50ea3..8b50ea3 100644 --- a/remotes/baseUriChangeFolder/folderInteger.json +++ b/json/remotes/baseUriChangeFolder/folderInteger.json diff --git a/remotes/baseUriChangeFolderInSubschema/folderInteger.json b/json/remotes/baseUriChangeFolderInSubschema/folderInteger.json index 8b50ea3..8b50ea3 100644 --- a/remotes/baseUriChangeFolderInSubschema/folderInteger.json +++ b/json/remotes/baseUriChangeFolderInSubschema/folderInteger.json diff --git a/remotes/different-id-ref-string.json b/json/remotes/different-id-ref-string.json index 7f88860..7f88860 100644 --- a/remotes/different-id-ref-string.json +++ b/json/remotes/different-id-ref-string.json diff --git a/remotes/draft-next/baseUriChange/folderInteger.json b/json/remotes/draft-next/baseUriChange/folderInteger.json index 388c881..388c881 100644 --- a/remotes/draft-next/baseUriChange/folderInteger.json +++ b/json/remotes/draft-next/baseUriChange/folderInteger.json diff --git a/remotes/draft-next/baseUriChangeFolder/folderInteger.json b/json/remotes/draft-next/baseUriChangeFolder/folderInteger.json index 388c881..388c881 100644 --- a/remotes/draft-next/baseUriChangeFolder/folderInteger.json +++ b/json/remotes/draft-next/baseUriChangeFolder/folderInteger.json diff --git a/remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json b/json/remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json index 388c881..388c881 100644 --- a/remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json +++ b/json/remotes/draft-next/baseUriChangeFolderInSubschema/folderInteger.json diff --git a/remotes/draft-next/extendible-dynamic-ref.json b/json/remotes/draft-next/extendible-dynamic-ref.json index e787aa3..e787aa3 100644 --- a/remotes/draft-next/extendible-dynamic-ref.json +++ b/json/remotes/draft-next/extendible-dynamic-ref.json diff --git a/remotes/draft-next/format-assertion-false.json b/json/remotes/draft-next/format-assertion-false.json index 91c8669..91c8669 100644 --- a/remotes/draft-next/format-assertion-false.json +++ b/json/remotes/draft-next/format-assertion-false.json diff --git a/remotes/draft-next/format-assertion-true.json b/json/remotes/draft-next/format-assertion-true.json index a33d143..a33d143 100644 --- a/remotes/draft-next/format-assertion-true.json +++ b/json/remotes/draft-next/format-assertion-true.json diff --git a/remotes/draft-next/integer.json b/json/remotes/draft-next/integer.json index 388c881..388c881 100644 --- a/remotes/draft-next/integer.json +++ b/json/remotes/draft-next/integer.json diff --git a/remotes/draft-next/locationIndependentIdentifier.json b/json/remotes/draft-next/locationIndependentIdentifier.json index 17b4df5..17b4df5 100644 --- a/remotes/draft-next/locationIndependentIdentifier.json +++ b/json/remotes/draft-next/locationIndependentIdentifier.json diff --git a/remotes/draft-next/metaschema-no-validation.json b/json/remotes/draft-next/metaschema-no-validation.json index c19c9e8..c19c9e8 100644 --- a/remotes/draft-next/metaschema-no-validation.json +++ b/json/remotes/draft-next/metaschema-no-validation.json diff --git a/remotes/draft-next/metaschema-optional-vocabulary.json b/json/remotes/draft-next/metaschema-optional-vocabulary.json index e78e531..e78e531 100644 --- a/remotes/draft-next/metaschema-optional-vocabulary.json +++ b/json/remotes/draft-next/metaschema-optional-vocabulary.json diff --git a/remotes/draft-next/name-defs.json b/json/remotes/draft-next/name-defs.json index cdb8c0c..cdb8c0c 100644 --- a/remotes/draft-next/name-defs.json +++ b/json/remotes/draft-next/name-defs.json diff --git a/remotes/draft-next/nested/foo-ref-string.json b/json/remotes/draft-next/nested/foo-ref-string.json index 50bf77f..50bf77f 100644 --- a/remotes/draft-next/nested/foo-ref-string.json +++ b/json/remotes/draft-next/nested/foo-ref-string.json diff --git a/remotes/draft-next/nested/string.json b/json/remotes/draft-next/nested/string.json index 7462207..7462207 100644 --- a/remotes/draft-next/nested/string.json +++ b/json/remotes/draft-next/nested/string.json diff --git a/remotes/draft-next/ref-and-defs.json b/json/remotes/draft-next/ref-and-defs.json index 46fafc9..46fafc9 100644 --- a/remotes/draft-next/ref-and-defs.json +++ b/json/remotes/draft-next/ref-and-defs.json diff --git a/remotes/draft-next/subSchemas-defs.json b/json/remotes/draft-next/subSchemas-defs.json index 75b7583..75b7583 100644 --- a/remotes/draft-next/subSchemas-defs.json +++ b/json/remotes/draft-next/subSchemas-defs.json diff --git a/remotes/draft-next/subSchemas.json b/json/remotes/draft-next/subSchemas.json index 575dd00..575dd00 100644 --- a/remotes/draft-next/subSchemas.json +++ b/json/remotes/draft-next/subSchemas.json diff --git a/remotes/draft-next/tree.json b/json/remotes/draft-next/tree.json index cad332a..cad332a 100644 --- a/remotes/draft-next/tree.json +++ b/json/remotes/draft-next/tree.json diff --git a/remotes/draft2019-09/baseUriChange/folderInteger.json b/json/remotes/draft2019-09/baseUriChange/folderInteger.json index bf679e8..bf679e8 100644 --- a/remotes/draft2019-09/baseUriChange/folderInteger.json +++ b/json/remotes/draft2019-09/baseUriChange/folderInteger.json diff --git a/remotes/draft2019-09/baseUriChangeFolder/folderInteger.json b/json/remotes/draft2019-09/baseUriChangeFolder/folderInteger.json index bf679e8..bf679e8 100644 --- a/remotes/draft2019-09/baseUriChangeFolder/folderInteger.json +++ b/json/remotes/draft2019-09/baseUriChangeFolder/folderInteger.json diff --git a/remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json b/json/remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json index bf679e8..bf679e8 100644 --- a/remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json +++ b/json/remotes/draft2019-09/baseUriChangeFolderInSubschema/folderInteger.json diff --git a/remotes/draft2019-09/dependentRequired.json b/json/remotes/draft2019-09/dependentRequired.json index 0d691d9..0d691d9 100644 --- a/remotes/draft2019-09/dependentRequired.json +++ b/json/remotes/draft2019-09/dependentRequired.json diff --git a/remotes/draft2019-09/extendible-dynamic-ref.json b/json/remotes/draft2019-09/extendible-dynamic-ref.json index c11bf0a..c11bf0a 100644 --- a/remotes/draft2019-09/extendible-dynamic-ref.json +++ b/json/remotes/draft2019-09/extendible-dynamic-ref.json diff --git a/remotes/draft2019-09/ignore-prefixItems.json b/json/remotes/draft2019-09/ignore-prefixItems.json index b5ef392..b5ef392 100644 --- a/remotes/draft2019-09/ignore-prefixItems.json +++ b/json/remotes/draft2019-09/ignore-prefixItems.json diff --git a/remotes/draft2019-09/integer.json b/json/remotes/draft2019-09/integer.json index bf679e8..bf679e8 100644 --- a/remotes/draft2019-09/integer.json +++ b/json/remotes/draft2019-09/integer.json diff --git a/remotes/draft2019-09/locationIndependentIdentifier.json b/json/remotes/draft2019-09/locationIndependentIdentifier.json index a0fb9fb..a0fb9fb 100644 --- a/remotes/draft2019-09/locationIndependentIdentifier.json +++ b/json/remotes/draft2019-09/locationIndependentIdentifier.json diff --git a/remotes/draft2019-09/metaschema-no-validation.json b/json/remotes/draft2019-09/metaschema-no-validation.json index 494f0ab..494f0ab 100644 --- a/remotes/draft2019-09/metaschema-no-validation.json +++ b/json/remotes/draft2019-09/metaschema-no-validation.json diff --git a/remotes/draft2019-09/metaschema-optional-vocabulary.json b/json/remotes/draft2019-09/metaschema-optional-vocabulary.json index 968597c..968597c 100644 --- a/remotes/draft2019-09/metaschema-optional-vocabulary.json +++ b/json/remotes/draft2019-09/metaschema-optional-vocabulary.json diff --git a/remotes/draft2019-09/name-defs.json b/json/remotes/draft2019-09/name-defs.json index 8892f16..8892f16 100644 --- a/remotes/draft2019-09/name-defs.json +++ b/json/remotes/draft2019-09/name-defs.json diff --git a/remotes/draft2019-09/nested/foo-ref-string.json b/json/remotes/draft2019-09/nested/foo-ref-string.json index fe10748..fe10748 100644 --- a/remotes/draft2019-09/nested/foo-ref-string.json +++ b/json/remotes/draft2019-09/nested/foo-ref-string.json diff --git a/remotes/draft2019-09/nested/string.json b/json/remotes/draft2019-09/nested/string.json index 242f713..242f713 100644 --- a/remotes/draft2019-09/nested/string.json +++ b/json/remotes/draft2019-09/nested/string.json diff --git a/remotes/draft2019-09/ref-and-defs.json b/json/remotes/draft2019-09/ref-and-defs.json index 0ad690d..0ad690d 100644 --- a/remotes/draft2019-09/ref-and-defs.json +++ b/json/remotes/draft2019-09/ref-and-defs.json diff --git a/remotes/draft2019-09/subSchemas-defs.json b/json/remotes/draft2019-09/subSchemas-defs.json index fdfee68..fdfee68 100644 --- a/remotes/draft2019-09/subSchemas-defs.json +++ b/json/remotes/draft2019-09/subSchemas-defs.json diff --git a/remotes/draft2019-09/subSchemas.json b/json/remotes/draft2019-09/subSchemas.json index 6dea225..6dea225 100644 --- a/remotes/draft2019-09/subSchemas.json +++ b/json/remotes/draft2019-09/subSchemas.json diff --git a/remotes/draft2019-09/tree.json b/json/remotes/draft2019-09/tree.json index fce7941..fce7941 100644 --- a/remotes/draft2019-09/tree.json +++ b/json/remotes/draft2019-09/tree.json diff --git a/remotes/draft2020-12/baseUriChange/folderInteger.json b/json/remotes/draft2020-12/baseUriChange/folderInteger.json index 1f44a63..1f44a63 100644 --- a/remotes/draft2020-12/baseUriChange/folderInteger.json +++ b/json/remotes/draft2020-12/baseUriChange/folderInteger.json diff --git a/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json b/json/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json index 1f44a63..1f44a63 100644 --- a/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json +++ b/json/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json diff --git a/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json b/json/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json index 1f44a63..1f44a63 100644 --- a/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json +++ b/json/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json diff --git a/remotes/draft2020-12/extendible-dynamic-ref.json b/json/remotes/draft2020-12/extendible-dynamic-ref.json index 65bc0c2..65bc0c2 100644 --- a/remotes/draft2020-12/extendible-dynamic-ref.json +++ b/json/remotes/draft2020-12/extendible-dynamic-ref.json diff --git a/remotes/draft2020-12/format-assertion-false.json b/json/remotes/draft2020-12/format-assertion-false.json index d6dd645..d6dd645 100644 --- a/remotes/draft2020-12/format-assertion-false.json +++ b/json/remotes/draft2020-12/format-assertion-false.json diff --git a/remotes/draft2020-12/format-assertion-true.json b/json/remotes/draft2020-12/format-assertion-true.json index bb16d58..bb16d58 100644 --- a/remotes/draft2020-12/format-assertion-true.json +++ b/json/remotes/draft2020-12/format-assertion-true.json diff --git a/remotes/draft2020-12/integer.json b/json/remotes/draft2020-12/integer.json index 1f44a63..1f44a63 100644 --- a/remotes/draft2020-12/integer.json +++ b/json/remotes/draft2020-12/integer.json diff --git a/remotes/draft2020-12/locationIndependentIdentifier.json b/json/remotes/draft2020-12/locationIndependentIdentifier.json index 6565a1e..6565a1e 100644 --- a/remotes/draft2020-12/locationIndependentIdentifier.json +++ b/json/remotes/draft2020-12/locationIndependentIdentifier.json diff --git a/remotes/draft2020-12/metaschema-no-validation.json b/json/remotes/draft2020-12/metaschema-no-validation.json index 85d74b2..85d74b2 100644 --- a/remotes/draft2020-12/metaschema-no-validation.json +++ b/json/remotes/draft2020-12/metaschema-no-validation.json diff --git a/remotes/draft2020-12/metaschema-optional-vocabulary.json b/json/remotes/draft2020-12/metaschema-optional-vocabulary.json index f38ec28..f38ec28 100644 --- a/remotes/draft2020-12/metaschema-optional-vocabulary.json +++ b/json/remotes/draft2020-12/metaschema-optional-vocabulary.json diff --git a/remotes/draft2020-12/name-defs.json b/json/remotes/draft2020-12/name-defs.json index 67bc33c..67bc33c 100644 --- a/remotes/draft2020-12/name-defs.json +++ b/json/remotes/draft2020-12/name-defs.json diff --git a/remotes/draft2020-12/nested/foo-ref-string.json b/json/remotes/draft2020-12/nested/foo-ref-string.json index 29661ff..29661ff 100644 --- a/remotes/draft2020-12/nested/foo-ref-string.json +++ b/json/remotes/draft2020-12/nested/foo-ref-string.json diff --git a/remotes/draft2020-12/nested/string.json b/json/remotes/draft2020-12/nested/string.json index 6607ac5..6607ac5 100644 --- a/remotes/draft2020-12/nested/string.json +++ b/json/remotes/draft2020-12/nested/string.json diff --git a/remotes/draft2020-12/prefixItems.json b/json/remotes/draft2020-12/prefixItems.json index acd8293..acd8293 100644 --- a/remotes/draft2020-12/prefixItems.json +++ b/json/remotes/draft2020-12/prefixItems.json diff --git a/remotes/draft2020-12/ref-and-defs.json b/json/remotes/draft2020-12/ref-and-defs.json index 16d30fa..16d30fa 100644 --- a/remotes/draft2020-12/ref-and-defs.json +++ b/json/remotes/draft2020-12/ref-and-defs.json diff --git a/remotes/draft2020-12/subSchemas-defs.json b/json/remotes/draft2020-12/subSchemas-defs.json index 1bb4846..1bb4846 100644 --- a/remotes/draft2020-12/subSchemas-defs.json +++ b/json/remotes/draft2020-12/subSchemas-defs.json diff --git a/remotes/draft2020-12/subSchemas.json b/json/remotes/draft2020-12/subSchemas.json index 5fca21d..5fca21d 100644 --- a/remotes/draft2020-12/subSchemas.json +++ b/json/remotes/draft2020-12/subSchemas.json diff --git a/remotes/draft2020-12/tree.json b/json/remotes/draft2020-12/tree.json index b07555f..b07555f 100644 --- a/remotes/draft2020-12/tree.json +++ b/json/remotes/draft2020-12/tree.json diff --git a/remotes/draft7/ignore-dependentRequired.json b/json/remotes/draft7/ignore-dependentRequired.json index 0ea927b..0ea927b 100644 --- a/remotes/draft7/ignore-dependentRequired.json +++ b/json/remotes/draft7/ignore-dependentRequired.json diff --git a/remotes/extendible-dynamic-ref.json b/json/remotes/extendible-dynamic-ref.json index d0bcd37..d0bcd37 100644 --- a/remotes/extendible-dynamic-ref.json +++ b/json/remotes/extendible-dynamic-ref.json diff --git a/remotes/integer.json b/json/remotes/integer.json index 8b50ea3..8b50ea3 100644 --- a/remotes/integer.json +++ b/json/remotes/integer.json diff --git a/remotes/locationIndependentIdentifier.json b/json/remotes/locationIndependentIdentifier.json index 96b17c3..96b17c3 100644 --- a/remotes/locationIndependentIdentifier.json +++ b/json/remotes/locationIndependentIdentifier.json diff --git a/remotes/locationIndependentIdentifierDraft4.json b/json/remotes/locationIndependentIdentifierDraft4.json index eeff1eb..eeff1eb 100644 --- a/remotes/locationIndependentIdentifierDraft4.json +++ b/json/remotes/locationIndependentIdentifierDraft4.json diff --git a/remotes/locationIndependentIdentifierPre2019.json b/json/remotes/locationIndependentIdentifierPre2019.json index e72815c..e72815c 100644 --- a/remotes/locationIndependentIdentifierPre2019.json +++ b/json/remotes/locationIndependentIdentifierPre2019.json diff --git a/remotes/name-defs.json b/json/remotes/name-defs.json index 1dab4a4..1dab4a4 100644 --- a/remotes/name-defs.json +++ b/json/remotes/name-defs.json diff --git a/remotes/name.json b/json/remotes/name.json index fceacb8..fceacb8 100644 --- a/remotes/name.json +++ b/json/remotes/name.json diff --git a/remotes/nested-absolute-ref-to-string.json b/json/remotes/nested-absolute-ref-to-string.json index f46c761..f46c761 100644 --- a/remotes/nested-absolute-ref-to-string.json +++ b/json/remotes/nested-absolute-ref-to-string.json diff --git a/remotes/nested/foo-ref-string.json b/json/remotes/nested/foo-ref-string.json index 9cd2527..9cd2527 100644 --- a/remotes/nested/foo-ref-string.json +++ b/json/remotes/nested/foo-ref-string.json diff --git a/remotes/nested/string.json b/json/remotes/nested/string.json index c2d48c0..c2d48c0 100644 --- a/remotes/nested/string.json +++ b/json/remotes/nested/string.json diff --git a/remotes/ref-and-definitions.json b/json/remotes/ref-and-definitions.json index e0ee802..e0ee802 100644 --- a/remotes/ref-and-definitions.json +++ b/json/remotes/ref-and-definitions.json diff --git a/remotes/ref-and-defs.json b/json/remotes/ref-and-defs.json index 85d06c3..85d06c3 100644 --- a/remotes/ref-and-defs.json +++ b/json/remotes/ref-and-defs.json diff --git a/remotes/subSchemas-defs.json b/json/remotes/subSchemas-defs.json index 50b7b6d..50b7b6d 100644 --- a/remotes/subSchemas-defs.json +++ b/json/remotes/subSchemas-defs.json diff --git a/remotes/subSchemas.json b/json/remotes/subSchemas.json index 9f8030b..9f8030b 100644 --- a/remotes/subSchemas.json +++ b/json/remotes/subSchemas.json diff --git a/remotes/tree.json b/json/remotes/tree.json index a12d98b..a12d98b 100644 --- a/remotes/tree.json +++ b/json/remotes/tree.json diff --git a/remotes/urn-ref-string.json b/json/remotes/urn-ref-string.json index aca2211..aca2211 100644 --- a/remotes/urn-ref-string.json +++ b/json/remotes/urn-ref-string.json diff --git a/test-schema.json b/json/test-schema.json index 8339316..8339316 100644 --- a/test-schema.json +++ b/json/test-schema.json diff --git a/tests/draft-next/additionalProperties.json b/json/tests/draft-next/additionalProperties.json index 7859fbb..7859fbb 100644 --- a/tests/draft-next/additionalProperties.json +++ b/json/tests/draft-next/additionalProperties.json diff --git a/tests/draft-next/allOf.json b/json/tests/draft-next/allOf.json index 86745da..86745da 100644 --- a/tests/draft-next/allOf.json +++ b/json/tests/draft-next/allOf.json diff --git a/tests/draft-next/anchor.json b/json/tests/draft-next/anchor.json index 321d844..321d844 100644 --- a/tests/draft-next/anchor.json +++ b/json/tests/draft-next/anchor.json diff --git a/tests/draft-next/anyOf.json b/json/tests/draft-next/anyOf.json index a97267c..a97267c 100644 --- a/tests/draft-next/anyOf.json +++ b/json/tests/draft-next/anyOf.json diff --git a/tests/draft-next/boolean_schema.json b/json/tests/draft-next/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft-next/boolean_schema.json +++ b/json/tests/draft-next/boolean_schema.json diff --git a/tests/draft-next/const.json b/json/tests/draft-next/const.json index 61fbd83..61fbd83 100644 --- a/tests/draft-next/const.json +++ b/json/tests/draft-next/const.json diff --git a/tests/draft-next/contains.json b/json/tests/draft-next/contains.json index c17f55e..c17f55e 100644 --- a/tests/draft-next/contains.json +++ b/json/tests/draft-next/contains.json diff --git a/tests/draft-next/content.json b/json/tests/draft-next/content.json index 37e1f09..37e1f09 100644 --- a/tests/draft-next/content.json +++ b/json/tests/draft-next/content.json diff --git a/tests/draft-next/default.json b/json/tests/draft-next/default.json index 689b68e..689b68e 100644 --- a/tests/draft-next/default.json +++ b/json/tests/draft-next/default.json diff --git a/tests/draft-next/defs.json b/json/tests/draft-next/defs.json index 7bda825..7bda825 100644 --- a/tests/draft-next/defs.json +++ b/json/tests/draft-next/defs.json diff --git a/tests/draft-next/dependentRequired.json b/json/tests/draft-next/dependentRequired.json index 2344724..2344724 100644 --- a/tests/draft-next/dependentRequired.json +++ b/json/tests/draft-next/dependentRequired.json diff --git a/tests/draft-next/dependentSchemas.json b/json/tests/draft-next/dependentSchemas.json index 67c7979..67c7979 100644 --- a/tests/draft-next/dependentSchemas.json +++ b/json/tests/draft-next/dependentSchemas.json diff --git a/tests/draft-next/dynamicRef.json b/json/tests/draft-next/dynamicRef.json index a4a7c44..a4a7c44 100644 --- a/tests/draft-next/dynamicRef.json +++ b/json/tests/draft-next/dynamicRef.json diff --git a/tests/draft-next/enum.json b/json/tests/draft-next/enum.json index 32e5af0..32e5af0 100644 --- a/tests/draft-next/enum.json +++ b/json/tests/draft-next/enum.json diff --git a/tests/draft-next/exclusiveMaximum.json b/json/tests/draft-next/exclusiveMaximum.json index 6ec4752..6ec4752 100644 --- a/tests/draft-next/exclusiveMaximum.json +++ b/json/tests/draft-next/exclusiveMaximum.json diff --git a/tests/draft-next/exclusiveMinimum.json b/json/tests/draft-next/exclusiveMinimum.json index 9026298..9026298 100644 --- a/tests/draft-next/exclusiveMinimum.json +++ b/json/tests/draft-next/exclusiveMinimum.json diff --git a/tests/draft-next/format.json b/json/tests/draft-next/format.json index 43a8bd6..43a8bd6 100644 --- a/tests/draft-next/format.json +++ b/json/tests/draft-next/format.json diff --git a/tests/draft-next/id.json b/json/tests/draft-next/id.json index 9b3a591..9b3a591 100644 --- a/tests/draft-next/id.json +++ b/json/tests/draft-next/id.json diff --git a/tests/draft-next/if-then-else.json b/json/tests/draft-next/if-then-else.json index 70576c4..70576c4 100644 --- a/tests/draft-next/if-then-else.json +++ b/json/tests/draft-next/if-then-else.json diff --git a/tests/draft-next/infinite-loop-detection.json b/json/tests/draft-next/infinite-loop-detection.json index fa66743..fa66743 100644 --- a/tests/draft-next/infinite-loop-detection.json +++ b/json/tests/draft-next/infinite-loop-detection.json diff --git a/tests/draft-next/items.json b/json/tests/draft-next/items.json index 459943b..459943b 100644 --- a/tests/draft-next/items.json +++ b/json/tests/draft-next/items.json diff --git a/tests/draft-next/maxContains.json b/json/tests/draft-next/maxContains.json index 7c15157..7c15157 100644 --- a/tests/draft-next/maxContains.json +++ b/json/tests/draft-next/maxContains.json diff --git a/tests/draft-next/maxItems.json b/json/tests/draft-next/maxItems.json index b215850..b215850 100644 --- a/tests/draft-next/maxItems.json +++ b/json/tests/draft-next/maxItems.json diff --git a/tests/draft-next/maxLength.json b/json/tests/draft-next/maxLength.json index e09e44a..e09e44a 100644 --- a/tests/draft-next/maxLength.json +++ b/json/tests/draft-next/maxLength.json diff --git a/tests/draft-next/maxProperties.json b/json/tests/draft-next/maxProperties.json index 5eec9da..5eec9da 100644 --- a/tests/draft-next/maxProperties.json +++ b/json/tests/draft-next/maxProperties.json diff --git a/tests/draft-next/maximum.json b/json/tests/draft-next/maximum.json index 656ce81..656ce81 100644 --- a/tests/draft-next/maximum.json +++ b/json/tests/draft-next/maximum.json diff --git a/tests/draft-next/minContains.json b/json/tests/draft-next/minContains.json index a6ad212..a6ad212 100644 --- a/tests/draft-next/minContains.json +++ b/json/tests/draft-next/minContains.json diff --git a/tests/draft-next/minItems.json b/json/tests/draft-next/minItems.json index b0e1c72..b0e1c72 100644 --- a/tests/draft-next/minItems.json +++ b/json/tests/draft-next/minItems.json diff --git a/tests/draft-next/minLength.json b/json/tests/draft-next/minLength.json index 16022ac..16022ac 100644 --- a/tests/draft-next/minLength.json +++ b/json/tests/draft-next/minLength.json diff --git a/tests/draft-next/minProperties.json b/json/tests/draft-next/minProperties.json index 434c0f1..434c0f1 100644 --- a/tests/draft-next/minProperties.json +++ b/json/tests/draft-next/minProperties.json diff --git a/tests/draft-next/minimum.json b/json/tests/draft-next/minimum.json index 2c6f71f..2c6f71f 100644 --- a/tests/draft-next/minimum.json +++ b/json/tests/draft-next/minimum.json diff --git a/tests/draft-next/multipleOf.json b/json/tests/draft-next/multipleOf.json index f153454..f153454 100644 --- a/tests/draft-next/multipleOf.json +++ b/json/tests/draft-next/multipleOf.json diff --git a/tests/draft-next/not.json b/json/tests/draft-next/not.json index b225104..b225104 100644 --- a/tests/draft-next/not.json +++ b/json/tests/draft-next/not.json diff --git a/tests/draft-next/oneOf.json b/json/tests/draft-next/oneOf.json index e8c0771..e8c0771 100644 --- a/tests/draft-next/oneOf.json +++ b/json/tests/draft-next/oneOf.json diff --git a/tests/draft-next/optional/bignum.json b/json/tests/draft-next/optional/bignum.json index 9f4f707..9f4f707 100644 --- a/tests/draft-next/optional/bignum.json +++ b/json/tests/draft-next/optional/bignum.json diff --git a/tests/draft-next/optional/dependencies-compatibility.json b/json/tests/draft-next/optional/dependencies-compatibility.json index 1fe384c..1fe384c 100644 --- a/tests/draft-next/optional/dependencies-compatibility.json +++ b/json/tests/draft-next/optional/dependencies-compatibility.json diff --git a/tests/draft-next/optional/ecmascript-regex.json b/json/tests/draft-next/optional/ecmascript-regex.json index 2721145..2721145 100644 --- a/tests/draft-next/optional/ecmascript-regex.json +++ b/json/tests/draft-next/optional/ecmascript-regex.json diff --git a/tests/draft-next/optional/float-overflow.json b/json/tests/draft-next/optional/float-overflow.json index a4fcca6..a4fcca6 100644 --- a/tests/draft-next/optional/float-overflow.json +++ b/json/tests/draft-next/optional/float-overflow.json diff --git a/tests/draft-next/optional/format-assertion.json b/json/tests/draft-next/optional/format-assertion.json index ede922a..ede922a 100644 --- a/tests/draft-next/optional/format-assertion.json +++ b/json/tests/draft-next/optional/format-assertion.json diff --git a/tests/draft-next/optional/format/date-time.json b/json/tests/draft-next/optional/format/date-time.json index e25845b..e25845b 100644 --- a/tests/draft-next/optional/format/date-time.json +++ b/json/tests/draft-next/optional/format/date-time.json diff --git a/tests/draft-next/optional/format/date.json b/json/tests/draft-next/optional/format/date.json index aa55555..aa55555 100644 --- a/tests/draft-next/optional/format/date.json +++ b/json/tests/draft-next/optional/format/date.json diff --git a/tests/draft-next/optional/format/duration.json b/json/tests/draft-next/optional/format/duration.json index d5adca2..d5adca2 100644 --- a/tests/draft-next/optional/format/duration.json +++ b/json/tests/draft-next/optional/format/duration.json diff --git a/tests/draft-next/optional/format/email.json b/json/tests/draft-next/optional/format/email.json index 5948ebd..5948ebd 100644 --- a/tests/draft-next/optional/format/email.json +++ b/json/tests/draft-next/optional/format/email.json diff --git a/tests/draft-next/optional/format/hostname.json b/json/tests/draft-next/optional/format/hostname.json index 9678486..9678486 100644 --- a/tests/draft-next/optional/format/hostname.json +++ b/json/tests/draft-next/optional/format/hostname.json diff --git a/tests/draft-next/optional/format/idn-email.json b/json/tests/draft-next/optional/format/idn-email.json index 1f35367..1f35367 100644 --- a/tests/draft-next/optional/format/idn-email.json +++ b/json/tests/draft-next/optional/format/idn-email.json diff --git a/tests/draft-next/optional/format/idn-hostname.json b/json/tests/draft-next/optional/format/idn-hostname.json index ee2e792..ee2e792 100644 --- a/tests/draft-next/optional/format/idn-hostname.json +++ b/json/tests/draft-next/optional/format/idn-hostname.json diff --git a/tests/draft-next/optional/format/ipv4.json b/json/tests/draft-next/optional/format/ipv4.json index e3e9440..e3e9440 100644 --- a/tests/draft-next/optional/format/ipv4.json +++ b/json/tests/draft-next/optional/format/ipv4.json diff --git a/tests/draft-next/optional/format/ipv6.json b/json/tests/draft-next/optional/format/ipv6.json index 241df7f..241df7f 100644 --- a/tests/draft-next/optional/format/ipv6.json +++ b/json/tests/draft-next/optional/format/ipv6.json diff --git a/tests/draft-next/optional/format/iri-reference.json b/json/tests/draft-next/optional/format/iri-reference.json index 543b99e..543b99e 100644 --- a/tests/draft-next/optional/format/iri-reference.json +++ b/json/tests/draft-next/optional/format/iri-reference.json diff --git a/tests/draft-next/optional/format/iri.json b/json/tests/draft-next/optional/format/iri.json index 48e82a2..48e82a2 100644 --- a/tests/draft-next/optional/format/iri.json +++ b/json/tests/draft-next/optional/format/iri.json diff --git a/tests/draft-next/optional/format/json-pointer.json b/json/tests/draft-next/optional/format/json-pointer.json index f55342f..f55342f 100644 --- a/tests/draft-next/optional/format/json-pointer.json +++ b/json/tests/draft-next/optional/format/json-pointer.json diff --git a/tests/draft-next/optional/format/regex.json b/json/tests/draft-next/optional/format/regex.json index 5987f53..5987f53 100644 --- a/tests/draft-next/optional/format/regex.json +++ b/json/tests/draft-next/optional/format/regex.json diff --git a/tests/draft-next/optional/format/relative-json-pointer.json b/json/tests/draft-next/optional/format/relative-json-pointer.json index 1e28fc2..1e28fc2 100644 --- a/tests/draft-next/optional/format/relative-json-pointer.json +++ b/json/tests/draft-next/optional/format/relative-json-pointer.json diff --git a/tests/draft-next/optional/format/time.json b/json/tests/draft-next/optional/format/time.json index 0a000a4..0a000a4 100644 --- a/tests/draft-next/optional/format/time.json +++ b/json/tests/draft-next/optional/format/time.json diff --git a/tests/draft-next/optional/format/uri-reference.json b/json/tests/draft-next/optional/format/uri-reference.json index 8d9d254..8d9d254 100644 --- a/tests/draft-next/optional/format/uri-reference.json +++ b/json/tests/draft-next/optional/format/uri-reference.json diff --git a/tests/draft-next/optional/format/uri-template.json b/json/tests/draft-next/optional/format/uri-template.json index f57d62d..f57d62d 100644 --- a/tests/draft-next/optional/format/uri-template.json +++ b/json/tests/draft-next/optional/format/uri-template.json diff --git a/tests/draft-next/optional/format/uri.json b/json/tests/draft-next/optional/format/uri.json index 50908ea..50908ea 100644 --- a/tests/draft-next/optional/format/uri.json +++ b/json/tests/draft-next/optional/format/uri.json diff --git a/tests/draft-next/optional/format/uuid.json b/json/tests/draft-next/optional/format/uuid.json index 6cea9da..6cea9da 100644 --- a/tests/draft-next/optional/format/uuid.json +++ b/json/tests/draft-next/optional/format/uuid.json diff --git a/tests/draft-next/optional/non-bmp-regex.json b/json/tests/draft-next/optional/non-bmp-regex.json index 3af875c..3af875c 100644 --- a/tests/draft-next/optional/non-bmp-regex.json +++ b/json/tests/draft-next/optional/non-bmp-regex.json diff --git a/tests/draft-next/optional/refOfUnknownKeyword.json b/json/tests/draft-next/optional/refOfUnknownKeyword.json index 489701c..489701c 100644 --- a/tests/draft-next/optional/refOfUnknownKeyword.json +++ b/json/tests/draft-next/optional/refOfUnknownKeyword.json diff --git a/tests/draft-next/pattern.json b/json/tests/draft-next/pattern.json index 09c6d0f..09c6d0f 100644 --- a/tests/draft-next/pattern.json +++ b/json/tests/draft-next/pattern.json diff --git a/tests/draft-next/patternProperties.json b/json/tests/draft-next/patternProperties.json index c7aca3d..c7aca3d 100644 --- a/tests/draft-next/patternProperties.json +++ b/json/tests/draft-next/patternProperties.json diff --git a/tests/draft-next/prefixItems.json b/json/tests/draft-next/prefixItems.json index a7f5928..a7f5928 100644 --- a/tests/draft-next/prefixItems.json +++ b/json/tests/draft-next/prefixItems.json diff --git a/tests/draft-next/properties.json b/json/tests/draft-next/properties.json index 1ba4fe8..1ba4fe8 100644 --- a/tests/draft-next/properties.json +++ b/json/tests/draft-next/properties.json diff --git a/tests/draft-next/propertyDependencies.json b/json/tests/draft-next/propertyDependencies.json index 9efa2b4..9efa2b4 100644 --- a/tests/draft-next/propertyDependencies.json +++ b/json/tests/draft-next/propertyDependencies.json diff --git a/tests/draft-next/propertyNames.json b/json/tests/draft-next/propertyNames.json index e614017..e614017 100644 --- a/tests/draft-next/propertyNames.json +++ b/json/tests/draft-next/propertyNames.json diff --git a/tests/draft-next/ref.json b/json/tests/draft-next/ref.json index 6457e8a..6457e8a 100644 --- a/tests/draft-next/ref.json +++ b/json/tests/draft-next/ref.json diff --git a/tests/draft-next/refRemote.json b/json/tests/draft-next/refRemote.json index 3768b53..3768b53 100644 --- a/tests/draft-next/refRemote.json +++ b/json/tests/draft-next/refRemote.json diff --git a/tests/draft-next/required.json b/json/tests/draft-next/required.json index cc02fd3..cc02fd3 100644 --- a/tests/draft-next/required.json +++ b/json/tests/draft-next/required.json diff --git a/tests/draft-next/type.json b/json/tests/draft-next/type.json index 86e551c..86e551c 100644 --- a/tests/draft-next/type.json +++ b/json/tests/draft-next/type.json diff --git a/tests/draft-next/unevaluatedItems.json b/json/tests/draft-next/unevaluatedItems.json index ce5d043..ce5d043 100644 --- a/tests/draft-next/unevaluatedItems.json +++ b/json/tests/draft-next/unevaluatedItems.json diff --git a/tests/draft-next/unevaluatedProperties.json b/json/tests/draft-next/unevaluatedProperties.json index f3d4012..f3d4012 100644 --- a/tests/draft-next/unevaluatedProperties.json +++ b/json/tests/draft-next/unevaluatedProperties.json diff --git a/tests/draft-next/uniqueItems.json b/json/tests/draft-next/uniqueItems.json index 610b257..610b257 100644 --- a/tests/draft-next/uniqueItems.json +++ b/json/tests/draft-next/uniqueItems.json diff --git a/tests/draft-next/unknownKeyword.json b/json/tests/draft-next/unknownKeyword.json index 055ff6b..055ff6b 100644 --- a/tests/draft-next/unknownKeyword.json +++ b/json/tests/draft-next/unknownKeyword.json diff --git a/tests/draft-next/vocabulary.json b/json/tests/draft-next/vocabulary.json index f25b9e1..f25b9e1 100644 --- a/tests/draft-next/vocabulary.json +++ b/json/tests/draft-next/vocabulary.json diff --git a/tests/draft2019-09/additionalItems.json b/json/tests/draft2019-09/additionalItems.json index aa44bcb..aa44bcb 100644 --- a/tests/draft2019-09/additionalItems.json +++ b/json/tests/draft2019-09/additionalItems.json diff --git a/tests/draft2019-09/additionalProperties.json b/json/tests/draft2019-09/additionalProperties.json index f9f03bb..f9f03bb 100644 --- a/tests/draft2019-09/additionalProperties.json +++ b/json/tests/draft2019-09/additionalProperties.json diff --git a/tests/draft2019-09/allOf.json b/json/tests/draft2019-09/allOf.json index dec267e..dec267e 100644 --- a/tests/draft2019-09/allOf.json +++ b/json/tests/draft2019-09/allOf.json diff --git a/tests/draft2019-09/anchor.json b/json/tests/draft2019-09/anchor.json index 5d8c86f..5d8c86f 100644 --- a/tests/draft2019-09/anchor.json +++ b/json/tests/draft2019-09/anchor.json diff --git a/tests/draft2019-09/anyOf.json b/json/tests/draft2019-09/anyOf.json index 8712d11..8712d11 100644 --- a/tests/draft2019-09/anyOf.json +++ b/json/tests/draft2019-09/anyOf.json diff --git a/tests/draft2019-09/boolean_schema.json b/json/tests/draft2019-09/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft2019-09/boolean_schema.json +++ b/json/tests/draft2019-09/boolean_schema.json diff --git a/tests/draft2019-09/const.json b/json/tests/draft2019-09/const.json index 29700fd..29700fd 100644 --- a/tests/draft2019-09/const.json +++ b/json/tests/draft2019-09/const.json diff --git a/tests/draft2019-09/contains.json b/json/tests/draft2019-09/contains.json index 30458bf..30458bf 100644 --- a/tests/draft2019-09/contains.json +++ b/json/tests/draft2019-09/contains.json diff --git a/tests/draft2019-09/content.json b/json/tests/draft2019-09/content.json index 2a7a5d8..2a7a5d8 100644 --- a/tests/draft2019-09/content.json +++ b/json/tests/draft2019-09/content.json diff --git a/tests/draft2019-09/default.json b/json/tests/draft2019-09/default.json index 95eba5a..95eba5a 100644 --- a/tests/draft2019-09/default.json +++ b/json/tests/draft2019-09/default.json diff --git a/tests/draft2019-09/defs.json b/json/tests/draft2019-09/defs.json index 3f9088c..3f9088c 100644 --- a/tests/draft2019-09/defs.json +++ b/json/tests/draft2019-09/defs.json diff --git a/tests/draft2019-09/dependentRequired.json b/json/tests/draft2019-09/dependentRequired.json index d573c10..d573c10 100644 --- a/tests/draft2019-09/dependentRequired.json +++ b/json/tests/draft2019-09/dependentRequired.json diff --git a/tests/draft2019-09/dependentSchemas.json b/json/tests/draft2019-09/dependentSchemas.json index b397583..b397583 100644 --- a/tests/draft2019-09/dependentSchemas.json +++ b/json/tests/draft2019-09/dependentSchemas.json diff --git a/tests/draft2019-09/enum.json b/json/tests/draft2019-09/enum.json index f9a44a6..f9a44a6 100644 --- a/tests/draft2019-09/enum.json +++ b/json/tests/draft2019-09/enum.json diff --git a/tests/draft2019-09/exclusiveMaximum.json b/json/tests/draft2019-09/exclusiveMaximum.json index 4ec8569..4ec8569 100644 --- a/tests/draft2019-09/exclusiveMaximum.json +++ b/json/tests/draft2019-09/exclusiveMaximum.json diff --git a/tests/draft2019-09/exclusiveMinimum.json b/json/tests/draft2019-09/exclusiveMinimum.json index 24f4689..24f4689 100644 --- a/tests/draft2019-09/exclusiveMinimum.json +++ b/json/tests/draft2019-09/exclusiveMinimum.json diff --git a/tests/draft2019-09/format.json b/json/tests/draft2019-09/format.json index 2934dce..2934dce 100644 --- a/tests/draft2019-09/format.json +++ b/json/tests/draft2019-09/format.json diff --git a/tests/draft2019-09/id.json b/json/tests/draft2019-09/id.json index e2e403f..e2e403f 100644 --- a/tests/draft2019-09/id.json +++ b/json/tests/draft2019-09/id.json diff --git a/tests/draft2019-09/if-then-else.json b/json/tests/draft2019-09/if-then-else.json index 510a0e0..510a0e0 100644 --- a/tests/draft2019-09/if-then-else.json +++ b/json/tests/draft2019-09/if-then-else.json diff --git a/tests/draft2019-09/infinite-loop-detection.json b/json/tests/draft2019-09/infinite-loop-detection.json index eb69414..eb69414 100644 --- a/tests/draft2019-09/infinite-loop-detection.json +++ b/json/tests/draft2019-09/infinite-loop-detection.json diff --git a/tests/draft2019-09/items.json b/json/tests/draft2019-09/items.json index e24156a..e24156a 100644 --- a/tests/draft2019-09/items.json +++ b/json/tests/draft2019-09/items.json diff --git a/tests/draft2019-09/maxContains.json b/json/tests/draft2019-09/maxContains.json index ce4507c..ce4507c 100644 --- a/tests/draft2019-09/maxContains.json +++ b/json/tests/draft2019-09/maxContains.json diff --git a/tests/draft2019-09/maxItems.json b/json/tests/draft2019-09/maxItems.json index d9ed157..d9ed157 100644 --- a/tests/draft2019-09/maxItems.json +++ b/json/tests/draft2019-09/maxItems.json diff --git a/tests/draft2019-09/maxLength.json b/json/tests/draft2019-09/maxLength.json index f242c3e..f242c3e 100644 --- a/tests/draft2019-09/maxLength.json +++ b/json/tests/draft2019-09/maxLength.json diff --git a/tests/draft2019-09/maxProperties.json b/json/tests/draft2019-09/maxProperties.json index 5b31474..5b31474 100644 --- a/tests/draft2019-09/maxProperties.json +++ b/json/tests/draft2019-09/maxProperties.json diff --git a/tests/draft2019-09/maximum.json b/json/tests/draft2019-09/maximum.json index c1f1dfd..c1f1dfd 100644 --- a/tests/draft2019-09/maximum.json +++ b/json/tests/draft2019-09/maximum.json diff --git a/tests/draft2019-09/minContains.json b/json/tests/draft2019-09/minContains.json index 8d3093c..8d3093c 100644 --- a/tests/draft2019-09/minContains.json +++ b/json/tests/draft2019-09/minContains.json diff --git a/tests/draft2019-09/minItems.json b/json/tests/draft2019-09/minItems.json index 07817cc..07817cc 100644 --- a/tests/draft2019-09/minItems.json +++ b/json/tests/draft2019-09/minItems.json diff --git a/tests/draft2019-09/minLength.json b/json/tests/draft2019-09/minLength.json index 19dec2c..19dec2c 100644 --- a/tests/draft2019-09/minLength.json +++ b/json/tests/draft2019-09/minLength.json diff --git a/tests/draft2019-09/minProperties.json b/json/tests/draft2019-09/minProperties.json index 20e01a9..20e01a9 100644 --- a/tests/draft2019-09/minProperties.json +++ b/json/tests/draft2019-09/minProperties.json diff --git a/tests/draft2019-09/minimum.json b/json/tests/draft2019-09/minimum.json index afb5f20..afb5f20 100644 --- a/tests/draft2019-09/minimum.json +++ b/json/tests/draft2019-09/minimum.json diff --git a/tests/draft2019-09/multipleOf.json b/json/tests/draft2019-09/multipleOf.json index 760a434..760a434 100644 --- a/tests/draft2019-09/multipleOf.json +++ b/json/tests/draft2019-09/multipleOf.json diff --git a/tests/draft2019-09/not.json b/json/tests/draft2019-09/not.json index 62c9af9..62c9af9 100644 --- a/tests/draft2019-09/not.json +++ b/json/tests/draft2019-09/not.json diff --git a/tests/draft2019-09/oneOf.json b/json/tests/draft2019-09/oneOf.json index 9b7a220..9b7a220 100644 --- a/tests/draft2019-09/oneOf.json +++ b/json/tests/draft2019-09/oneOf.json diff --git a/tests/draft2019-09/optional/bignum.json b/json/tests/draft2019-09/optional/bignum.json index 8b06467..8b06467 100644 --- a/tests/draft2019-09/optional/bignum.json +++ b/json/tests/draft2019-09/optional/bignum.json diff --git a/tests/draft2019-09/optional/cross-draft.json b/json/tests/draft2019-09/optional/cross-draft.json index efd3f87..efd3f87 100644 --- a/tests/draft2019-09/optional/cross-draft.json +++ b/json/tests/draft2019-09/optional/cross-draft.json diff --git a/tests/draft2019-09/optional/dependencies-compatibility.json b/json/tests/draft2019-09/optional/dependencies-compatibility.json index 5bfbd05..5bfbd05 100644 --- a/tests/draft2019-09/optional/dependencies-compatibility.json +++ b/json/tests/draft2019-09/optional/dependencies-compatibility.json diff --git a/tests/draft2019-09/optional/ecmascript-regex.json b/json/tests/draft2019-09/optional/ecmascript-regex.json index be1059c..be1059c 100644 --- a/tests/draft2019-09/optional/ecmascript-regex.json +++ b/json/tests/draft2019-09/optional/ecmascript-regex.json diff --git a/tests/draft2019-09/optional/float-overflow.json b/json/tests/draft2019-09/optional/float-overflow.json index f5741fa..f5741fa 100644 --- a/tests/draft2019-09/optional/float-overflow.json +++ b/json/tests/draft2019-09/optional/float-overflow.json diff --git a/tests/draft2019-09/optional/format/date-time.json b/json/tests/draft2019-09/optional/format/date-time.json index 731001d..731001d 100644 --- a/tests/draft2019-09/optional/format/date-time.json +++ b/json/tests/draft2019-09/optional/format/date-time.json diff --git a/tests/draft2019-09/optional/format/date.json b/json/tests/draft2019-09/optional/format/date.json index 805888c..805888c 100644 --- a/tests/draft2019-09/optional/format/date.json +++ b/json/tests/draft2019-09/optional/format/date.json diff --git a/tests/draft2019-09/optional/format/duration.json b/json/tests/draft2019-09/optional/format/duration.json index 00d5f47..00d5f47 100644 --- a/tests/draft2019-09/optional/format/duration.json +++ b/json/tests/draft2019-09/optional/format/duration.json diff --git a/tests/draft2019-09/optional/format/email.json b/json/tests/draft2019-09/optional/format/email.json index e6acc32..e6acc32 100644 --- a/tests/draft2019-09/optional/format/email.json +++ b/json/tests/draft2019-09/optional/format/email.json diff --git a/tests/draft2019-09/optional/format/hostname.json b/json/tests/draft2019-09/optional/format/hostname.json index eac8cac..eac8cac 100644 --- a/tests/draft2019-09/optional/format/hostname.json +++ b/json/tests/draft2019-09/optional/format/hostname.json diff --git a/tests/draft2019-09/optional/format/idn-email.json b/json/tests/draft2019-09/optional/format/idn-email.json index baf01d0..baf01d0 100644 --- a/tests/draft2019-09/optional/format/idn-email.json +++ b/json/tests/draft2019-09/optional/format/idn-email.json diff --git a/tests/draft2019-09/optional/format/idn-hostname.json b/json/tests/draft2019-09/optional/format/idn-hostname.json index 72f1797..72f1797 100644 --- a/tests/draft2019-09/optional/format/idn-hostname.json +++ b/json/tests/draft2019-09/optional/format/idn-hostname.json diff --git a/tests/draft2019-09/optional/format/ipv4.json b/json/tests/draft2019-09/optional/format/ipv4.json index ac1e14c..ac1e14c 100644 --- a/tests/draft2019-09/optional/format/ipv4.json +++ b/json/tests/draft2019-09/optional/format/ipv4.json diff --git a/tests/draft2019-09/optional/format/ipv6.json b/json/tests/draft2019-09/optional/format/ipv6.json index 0486091..0486091 100644 --- a/tests/draft2019-09/optional/format/ipv6.json +++ b/json/tests/draft2019-09/optional/format/ipv6.json diff --git a/tests/draft2019-09/optional/format/iri-reference.json b/json/tests/draft2019-09/optional/format/iri-reference.json index 6914210..6914210 100644 --- a/tests/draft2019-09/optional/format/iri-reference.json +++ b/json/tests/draft2019-09/optional/format/iri-reference.json diff --git a/tests/draft2019-09/optional/format/iri.json b/json/tests/draft2019-09/optional/format/iri.json index ad4c79e..ad4c79e 100644 --- a/tests/draft2019-09/optional/format/iri.json +++ b/json/tests/draft2019-09/optional/format/iri.json diff --git a/tests/draft2019-09/optional/format/json-pointer.json b/json/tests/draft2019-09/optional/format/json-pointer.json index 39f1cc9..39f1cc9 100644 --- a/tests/draft2019-09/optional/format/json-pointer.json +++ b/json/tests/draft2019-09/optional/format/json-pointer.json diff --git a/tests/draft2019-09/optional/format/regex.json b/json/tests/draft2019-09/optional/format/regex.json index da32401..da32401 100644 --- a/tests/draft2019-09/optional/format/regex.json +++ b/json/tests/draft2019-09/optional/format/regex.json diff --git a/tests/draft2019-09/optional/format/relative-json-pointer.json b/json/tests/draft2019-09/optional/format/relative-json-pointer.json index ba97d2a..ba97d2a 100644 --- a/tests/draft2019-09/optional/format/relative-json-pointer.json +++ b/json/tests/draft2019-09/optional/format/relative-json-pointer.json diff --git a/tests/draft2019-09/optional/format/time.json b/json/tests/draft2019-09/optional/format/time.json index dadaae6..dadaae6 100644 --- a/tests/draft2019-09/optional/format/time.json +++ b/json/tests/draft2019-09/optional/format/time.json diff --git a/tests/draft2019-09/optional/format/unknown.json b/json/tests/draft2019-09/optional/format/unknown.json index c89f730..c89f730 100644 --- a/tests/draft2019-09/optional/format/unknown.json +++ b/json/tests/draft2019-09/optional/format/unknown.json diff --git a/tests/draft2019-09/optional/format/uri-reference.json b/json/tests/draft2019-09/optional/format/uri-reference.json index 2c49da6..2c49da6 100644 --- a/tests/draft2019-09/optional/format/uri-reference.json +++ b/json/tests/draft2019-09/optional/format/uri-reference.json diff --git a/tests/draft2019-09/optional/format/uri-template.json b/json/tests/draft2019-09/optional/format/uri-template.json index f4aee10..f4aee10 100644 --- a/tests/draft2019-09/optional/format/uri-template.json +++ b/json/tests/draft2019-09/optional/format/uri-template.json diff --git a/tests/draft2019-09/optional/format/uri.json b/json/tests/draft2019-09/optional/format/uri.json index ad67840..ad67840 100644 --- a/tests/draft2019-09/optional/format/uri.json +++ b/json/tests/draft2019-09/optional/format/uri.json diff --git a/tests/draft2019-09/optional/format/uuid.json b/json/tests/draft2019-09/optional/format/uuid.json index dc6fb7e..dc6fb7e 100644 --- a/tests/draft2019-09/optional/format/uuid.json +++ b/json/tests/draft2019-09/optional/format/uuid.json diff --git a/tests/draft2019-09/optional/no-schema.json b/json/tests/draft2019-09/optional/no-schema.json index 676e6b5..676e6b5 100644 --- a/tests/draft2019-09/optional/no-schema.json +++ b/json/tests/draft2019-09/optional/no-schema.json diff --git a/tests/draft2019-09/optional/non-bmp-regex.json b/json/tests/draft2019-09/optional/non-bmp-regex.json index ef25000..ef25000 100644 --- a/tests/draft2019-09/optional/non-bmp-regex.json +++ b/json/tests/draft2019-09/optional/non-bmp-regex.json diff --git a/tests/draft2019-09/optional/refOfUnknownKeyword.json b/json/tests/draft2019-09/optional/refOfUnknownKeyword.json index eee1c33..eee1c33 100644 --- a/tests/draft2019-09/optional/refOfUnknownKeyword.json +++ b/json/tests/draft2019-09/optional/refOfUnknownKeyword.json diff --git a/tests/draft2019-09/pattern.json b/json/tests/draft2019-09/pattern.json index cfb8774..cfb8774 100644 --- a/tests/draft2019-09/pattern.json +++ b/json/tests/draft2019-09/pattern.json diff --git a/tests/draft2019-09/patternProperties.json b/json/tests/draft2019-09/patternProperties.json index 354bb48..354bb48 100644 --- a/tests/draft2019-09/patternProperties.json +++ b/json/tests/draft2019-09/patternProperties.json diff --git a/tests/draft2019-09/properties.json b/json/tests/draft2019-09/properties.json index a53429c..a53429c 100644 --- a/tests/draft2019-09/properties.json +++ b/json/tests/draft2019-09/properties.json diff --git a/tests/draft2019-09/propertyNames.json b/json/tests/draft2019-09/propertyNames.json index b7fecbf..b7fecbf 100644 --- a/tests/draft2019-09/propertyNames.json +++ b/json/tests/draft2019-09/propertyNames.json diff --git a/tests/draft2019-09/recursiveRef.json b/json/tests/draft2019-09/recursiveRef.json index 1e713ff..1e713ff 100644 --- a/tests/draft2019-09/recursiveRef.json +++ b/json/tests/draft2019-09/recursiveRef.json diff --git a/tests/draft2019-09/ref.json b/json/tests/draft2019-09/ref.json index d420bd9..d420bd9 100644 --- a/tests/draft2019-09/ref.json +++ b/json/tests/draft2019-09/ref.json diff --git a/tests/draft2019-09/refRemote.json b/json/tests/draft2019-09/refRemote.json index 79107f9..79107f9 100644 --- a/tests/draft2019-09/refRemote.json +++ b/json/tests/draft2019-09/refRemote.json diff --git a/tests/draft2019-09/required.json b/json/tests/draft2019-09/required.json index bca98a9..bca98a9 100644 --- a/tests/draft2019-09/required.json +++ b/json/tests/draft2019-09/required.json diff --git a/tests/draft2019-09/type.json b/json/tests/draft2019-09/type.json index 92c6be8..92c6be8 100644 --- a/tests/draft2019-09/type.json +++ b/json/tests/draft2019-09/type.json diff --git a/tests/draft2019-09/unevaluatedItems.json b/json/tests/draft2019-09/unevaluatedItems.json index a0b1f33..a0b1f33 100644 --- a/tests/draft2019-09/unevaluatedItems.json +++ b/json/tests/draft2019-09/unevaluatedItems.json diff --git a/tests/draft2019-09/unevaluatedProperties.json b/json/tests/draft2019-09/unevaluatedProperties.json index fdb4ac9..fdb4ac9 100644 --- a/tests/draft2019-09/unevaluatedProperties.json +++ b/json/tests/draft2019-09/unevaluatedProperties.json diff --git a/tests/draft2019-09/uniqueItems.json b/json/tests/draft2019-09/uniqueItems.json index 6da878b..6da878b 100644 --- a/tests/draft2019-09/uniqueItems.json +++ b/json/tests/draft2019-09/uniqueItems.json diff --git a/tests/draft2019-09/unknownKeyword.json b/json/tests/draft2019-09/unknownKeyword.json index f98e87c..f98e87c 100644 --- a/tests/draft2019-09/unknownKeyword.json +++ b/json/tests/draft2019-09/unknownKeyword.json diff --git a/tests/draft2019-09/vocabulary.json b/json/tests/draft2019-09/vocabulary.json index 98482b2..98482b2 100644 --- a/tests/draft2019-09/vocabulary.json +++ b/json/tests/draft2019-09/vocabulary.json diff --git a/tests/draft2020-12/additionalProperties.json b/json/tests/draft2020-12/additionalProperties.json index 29e69c1..29e69c1 100644 --- a/tests/draft2020-12/additionalProperties.json +++ b/json/tests/draft2020-12/additionalProperties.json diff --git a/tests/draft2020-12/allOf.json b/json/tests/draft2020-12/allOf.json index 9e87903..9e87903 100644 --- a/tests/draft2020-12/allOf.json +++ b/json/tests/draft2020-12/allOf.json diff --git a/tests/draft2020-12/anchor.json b/json/tests/draft2020-12/anchor.json index 423835d..423835d 100644 --- a/tests/draft2020-12/anchor.json +++ b/json/tests/draft2020-12/anchor.json diff --git a/tests/draft2020-12/anyOf.json b/json/tests/draft2020-12/anyOf.json index 89b192d..89b192d 100644 --- a/tests/draft2020-12/anyOf.json +++ b/json/tests/draft2020-12/anyOf.json diff --git a/tests/draft2020-12/boolean_schema.json b/json/tests/draft2020-12/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft2020-12/boolean_schema.json +++ b/json/tests/draft2020-12/boolean_schema.json diff --git a/tests/draft2020-12/const.json b/json/tests/draft2020-12/const.json index 50be86a..50be86a 100644 --- a/tests/draft2020-12/const.json +++ b/json/tests/draft2020-12/const.json diff --git a/tests/draft2020-12/contains.json b/json/tests/draft2020-12/contains.json index 08a00a7..08a00a7 100644 --- a/tests/draft2020-12/contains.json +++ b/json/tests/draft2020-12/contains.json diff --git a/tests/draft2020-12/content.json b/json/tests/draft2020-12/content.json index 698f780..698f780 100644 --- a/tests/draft2020-12/content.json +++ b/json/tests/draft2020-12/content.json diff --git a/tests/draft2020-12/default.json b/json/tests/draft2020-12/default.json index ceb3ae2..ceb3ae2 100644 --- a/tests/draft2020-12/default.json +++ b/json/tests/draft2020-12/default.json diff --git a/tests/draft2020-12/defs.json b/json/tests/draft2020-12/defs.json index da2a503..da2a503 100644 --- a/tests/draft2020-12/defs.json +++ b/json/tests/draft2020-12/defs.json diff --git a/tests/draft2020-12/dependentRequired.json b/json/tests/draft2020-12/dependentRequired.json index 2baa38e..2baa38e 100644 --- a/tests/draft2020-12/dependentRequired.json +++ b/json/tests/draft2020-12/dependentRequired.json diff --git a/tests/draft2020-12/dependentSchemas.json b/json/tests/draft2020-12/dependentSchemas.json index bf7aa53..bf7aa53 100644 --- a/tests/draft2020-12/dependentSchemas.json +++ b/json/tests/draft2020-12/dependentSchemas.json diff --git a/tests/draft2020-12/dynamicRef.json b/json/tests/draft2020-12/dynamicRef.json index 0f6ed48..0f6ed48 100644 --- a/tests/draft2020-12/dynamicRef.json +++ b/json/tests/draft2020-12/dynamicRef.json diff --git a/tests/draft2020-12/enum.json b/json/tests/draft2020-12/enum.json index 0d780b2..0d780b2 100644 --- a/tests/draft2020-12/enum.json +++ b/json/tests/draft2020-12/enum.json diff --git a/tests/draft2020-12/exclusiveMaximum.json b/json/tests/draft2020-12/exclusiveMaximum.json index 05db233..05db233 100644 --- a/tests/draft2020-12/exclusiveMaximum.json +++ b/json/tests/draft2020-12/exclusiveMaximum.json diff --git a/tests/draft2020-12/exclusiveMinimum.json b/json/tests/draft2020-12/exclusiveMinimum.json index 00af9d7..00af9d7 100644 --- a/tests/draft2020-12/exclusiveMinimum.json +++ b/json/tests/draft2020-12/exclusiveMinimum.json diff --git a/tests/draft2020-12/format.json b/json/tests/draft2020-12/format.json index 6b7904e..6b7904e 100644 --- a/tests/draft2020-12/format.json +++ b/json/tests/draft2020-12/format.json diff --git a/tests/draft2020-12/id.json b/json/tests/draft2020-12/id.json index 0ae5fe6..0ae5fe6 100644 --- a/tests/draft2020-12/id.json +++ b/json/tests/draft2020-12/id.json diff --git a/tests/draft2020-12/if-then-else.json b/json/tests/draft2020-12/if-then-else.json index 1c35d7e..1c35d7e 100644 --- a/tests/draft2020-12/if-then-else.json +++ b/json/tests/draft2020-12/if-then-else.json diff --git a/tests/draft2020-12/infinite-loop-detection.json b/json/tests/draft2020-12/infinite-loop-detection.json index 46f157a..46f157a 100644 --- a/tests/draft2020-12/infinite-loop-detection.json +++ b/json/tests/draft2020-12/infinite-loop-detection.json diff --git a/tests/draft2020-12/items.json b/json/tests/draft2020-12/items.json index 1ef18bd..1ef18bd 100644 --- a/tests/draft2020-12/items.json +++ b/json/tests/draft2020-12/items.json diff --git a/tests/draft2020-12/maxContains.json b/json/tests/draft2020-12/maxContains.json index 8cd3ca7..8cd3ca7 100644 --- a/tests/draft2020-12/maxContains.json +++ b/json/tests/draft2020-12/maxContains.json diff --git a/tests/draft2020-12/maxItems.json b/json/tests/draft2020-12/maxItems.json index f6a6b7c..f6a6b7c 100644 --- a/tests/draft2020-12/maxItems.json +++ b/json/tests/draft2020-12/maxItems.json diff --git a/tests/draft2020-12/maxLength.json b/json/tests/draft2020-12/maxLength.json index b6eb034..b6eb034 100644 --- a/tests/draft2020-12/maxLength.json +++ b/json/tests/draft2020-12/maxLength.json diff --git a/tests/draft2020-12/maxProperties.json b/json/tests/draft2020-12/maxProperties.json index 73ae731..73ae731 100644 --- a/tests/draft2020-12/maxProperties.json +++ b/json/tests/draft2020-12/maxProperties.json diff --git a/tests/draft2020-12/maximum.json b/json/tests/draft2020-12/maximum.json index b99a541..b99a541 100644 --- a/tests/draft2020-12/maximum.json +++ b/json/tests/draft2020-12/maximum.json diff --git a/tests/draft2020-12/minContains.json b/json/tests/draft2020-12/minContains.json index ee72d7d..ee72d7d 100644 --- a/tests/draft2020-12/minContains.json +++ b/json/tests/draft2020-12/minContains.json diff --git a/tests/draft2020-12/minItems.json b/json/tests/draft2020-12/minItems.json index 9d6a8b6..9d6a8b6 100644 --- a/tests/draft2020-12/minItems.json +++ b/json/tests/draft2020-12/minItems.json diff --git a/tests/draft2020-12/minLength.json b/json/tests/draft2020-12/minLength.json index e0930b6..e0930b6 100644 --- a/tests/draft2020-12/minLength.json +++ b/json/tests/draft2020-12/minLength.json diff --git a/tests/draft2020-12/minProperties.json b/json/tests/draft2020-12/minProperties.json index a753ad3..a753ad3 100644 --- a/tests/draft2020-12/minProperties.json +++ b/json/tests/draft2020-12/minProperties.json diff --git a/tests/draft2020-12/minimum.json b/json/tests/draft2020-12/minimum.json index dc44052..dc44052 100644 --- a/tests/draft2020-12/minimum.json +++ b/json/tests/draft2020-12/minimum.json diff --git a/tests/draft2020-12/multipleOf.json b/json/tests/draft2020-12/multipleOf.json index 92d6979..92d6979 100644 --- a/tests/draft2020-12/multipleOf.json +++ b/json/tests/draft2020-12/multipleOf.json diff --git a/tests/draft2020-12/not.json b/json/tests/draft2020-12/not.json index 57e45ba..57e45ba 100644 --- a/tests/draft2020-12/not.json +++ b/json/tests/draft2020-12/not.json diff --git a/tests/draft2020-12/oneOf.json b/json/tests/draft2020-12/oneOf.json index 416c8e5..416c8e5 100644 --- a/tests/draft2020-12/oneOf.json +++ b/json/tests/draft2020-12/oneOf.json diff --git a/tests/draft2020-12/optional/bignum.json b/json/tests/draft2020-12/optional/bignum.json index d69b29e..d69b29e 100644 --- a/tests/draft2020-12/optional/bignum.json +++ b/json/tests/draft2020-12/optional/bignum.json diff --git a/tests/draft2020-12/optional/cross-draft.json b/json/tests/draft2020-12/optional/cross-draft.json index 5113bd6..5113bd6 100644 --- a/tests/draft2020-12/optional/cross-draft.json +++ b/json/tests/draft2020-12/optional/cross-draft.json diff --git a/tests/draft2020-12/optional/dependencies-compatibility.json b/json/tests/draft2020-12/optional/dependencies-compatibility.json index 47d5bd7..47d5bd7 100644 --- a/tests/draft2020-12/optional/dependencies-compatibility.json +++ b/json/tests/draft2020-12/optional/dependencies-compatibility.json diff --git a/tests/draft2020-12/optional/ecmascript-regex.json b/json/tests/draft2020-12/optional/ecmascript-regex.json index 23b962e..23b962e 100644 --- a/tests/draft2020-12/optional/ecmascript-regex.json +++ b/json/tests/draft2020-12/optional/ecmascript-regex.json diff --git a/tests/draft2020-12/optional/float-overflow.json b/json/tests/draft2020-12/optional/float-overflow.json index f5ae8b1..f5ae8b1 100644 --- a/tests/draft2020-12/optional/float-overflow.json +++ b/json/tests/draft2020-12/optional/float-overflow.json diff --git a/tests/draft2020-12/optional/format-assertion.json b/json/tests/draft2020-12/optional/format-assertion.json index 0340037..0340037 100644 --- a/tests/draft2020-12/optional/format-assertion.json +++ b/json/tests/draft2020-12/optional/format-assertion.json diff --git a/tests/draft2020-12/optional/format/date-time.json b/json/tests/draft2020-12/optional/format/date-time.json index 8783d73..8783d73 100644 --- a/tests/draft2020-12/optional/format/date-time.json +++ b/json/tests/draft2020-12/optional/format/date-time.json diff --git a/tests/draft2020-12/optional/format/date.json b/json/tests/draft2020-12/optional/format/date.json index dfb1c80..dfb1c80 100644 --- a/tests/draft2020-12/optional/format/date.json +++ b/json/tests/draft2020-12/optional/format/date.json diff --git a/tests/draft2020-12/optional/format/duration.json b/json/tests/draft2020-12/optional/format/duration.json index a3af56e..a3af56e 100644 --- a/tests/draft2020-12/optional/format/duration.json +++ b/json/tests/draft2020-12/optional/format/duration.json diff --git a/tests/draft2020-12/optional/format/email.json b/json/tests/draft2020-12/optional/format/email.json index ee07589..ee07589 100644 --- a/tests/draft2020-12/optional/format/email.json +++ b/json/tests/draft2020-12/optional/format/email.json diff --git a/tests/draft2020-12/optional/format/hostname.json b/json/tests/draft2020-12/optional/format/hostname.json index c8db977..c8db977 100644 --- a/tests/draft2020-12/optional/format/hostname.json +++ b/json/tests/draft2020-12/optional/format/hostname.json diff --git a/tests/draft2020-12/optional/format/idn-email.json b/json/tests/draft2020-12/optional/format/idn-email.json index 50f3c23..50f3c23 100644 --- a/tests/draft2020-12/optional/format/idn-email.json +++ b/json/tests/draft2020-12/optional/format/idn-email.json diff --git a/tests/draft2020-12/optional/format/idn-hostname.json b/json/tests/draft2020-12/optional/format/idn-hostname.json index 5549c05..5549c05 100644 --- a/tests/draft2020-12/optional/format/idn-hostname.json +++ b/json/tests/draft2020-12/optional/format/idn-hostname.json diff --git a/tests/draft2020-12/optional/format/ipv4.json b/json/tests/draft2020-12/optional/format/ipv4.json index c72b6fc..c72b6fc 100644 --- a/tests/draft2020-12/optional/format/ipv4.json +++ b/json/tests/draft2020-12/optional/format/ipv4.json diff --git a/tests/draft2020-12/optional/format/ipv6.json b/json/tests/draft2020-12/optional/format/ipv6.json index b9e570c..b9e570c 100644 --- a/tests/draft2020-12/optional/format/ipv6.json +++ b/json/tests/draft2020-12/optional/format/ipv6.json diff --git a/tests/draft2020-12/optional/format/iri-reference.json b/json/tests/draft2020-12/optional/format/iri-reference.json index 0c9483d..0c9483d 100644 --- a/tests/draft2020-12/optional/format/iri-reference.json +++ b/json/tests/draft2020-12/optional/format/iri-reference.json diff --git a/tests/draft2020-12/optional/format/iri.json b/json/tests/draft2020-12/optional/format/iri.json index 311c9ef..311c9ef 100644 --- a/tests/draft2020-12/optional/format/iri.json +++ b/json/tests/draft2020-12/optional/format/iri.json diff --git a/tests/draft2020-12/optional/format/json-pointer.json b/json/tests/draft2020-12/optional/format/json-pointer.json index 71ba9b6..71ba9b6 100644 --- a/tests/draft2020-12/optional/format/json-pointer.json +++ b/json/tests/draft2020-12/optional/format/json-pointer.json diff --git a/tests/draft2020-12/optional/format/regex.json b/json/tests/draft2020-12/optional/format/regex.json index a036c6d..a036c6d 100644 --- a/tests/draft2020-12/optional/format/regex.json +++ b/json/tests/draft2020-12/optional/format/regex.json diff --git a/tests/draft2020-12/optional/format/relative-json-pointer.json b/json/tests/draft2020-12/optional/format/relative-json-pointer.json index 3eaf9ce..3eaf9ce 100644 --- a/tests/draft2020-12/optional/format/relative-json-pointer.json +++ b/json/tests/draft2020-12/optional/format/relative-json-pointer.json diff --git a/tests/draft2020-12/optional/format/time.json b/json/tests/draft2020-12/optional/format/time.json index 8967932..8967932 100644 --- a/tests/draft2020-12/optional/format/time.json +++ b/json/tests/draft2020-12/optional/format/time.json diff --git a/tests/draft2020-12/optional/format/unknown.json b/json/tests/draft2020-12/optional/format/unknown.json index 7fc35f5..7fc35f5 100644 --- a/tests/draft2020-12/optional/format/unknown.json +++ b/json/tests/draft2020-12/optional/format/unknown.json diff --git a/tests/draft2020-12/optional/format/uri-reference.json b/json/tests/draft2020-12/optional/format/uri-reference.json index 46f28e6..46f28e6 100644 --- a/tests/draft2020-12/optional/format/uri-reference.json +++ b/json/tests/draft2020-12/optional/format/uri-reference.json diff --git a/tests/draft2020-12/optional/format/uri-template.json b/json/tests/draft2020-12/optional/format/uri-template.json index 08aab82..08aab82 100644 --- a/tests/draft2020-12/optional/format/uri-template.json +++ b/json/tests/draft2020-12/optional/format/uri-template.json diff --git a/tests/draft2020-12/optional/format/uri.json b/json/tests/draft2020-12/optional/format/uri.json index 84b5f15..84b5f15 100644 --- a/tests/draft2020-12/optional/format/uri.json +++ b/json/tests/draft2020-12/optional/format/uri.json diff --git a/tests/draft2020-12/optional/format/uuid.json b/json/tests/draft2020-12/optional/format/uuid.json index d152643..d152643 100644 --- a/tests/draft2020-12/optional/format/uuid.json +++ b/json/tests/draft2020-12/optional/format/uuid.json diff --git a/tests/draft2020-12/optional/no-schema.json b/json/tests/draft2020-12/optional/no-schema.json index 676e6b5..676e6b5 100644 --- a/tests/draft2020-12/optional/no-schema.json +++ b/json/tests/draft2020-12/optional/no-schema.json diff --git a/tests/draft2020-12/optional/non-bmp-regex.json b/json/tests/draft2020-12/optional/non-bmp-regex.json index d2efb3e..d2efb3e 100644 --- a/tests/draft2020-12/optional/non-bmp-regex.json +++ b/json/tests/draft2020-12/optional/non-bmp-regex.json diff --git a/tests/draft2020-12/optional/refOfUnknownKeyword.json b/json/tests/draft2020-12/optional/refOfUnknownKeyword.json index f91c188..f91c188 100644 --- a/tests/draft2020-12/optional/refOfUnknownKeyword.json +++ b/json/tests/draft2020-12/optional/refOfUnknownKeyword.json diff --git a/tests/draft2020-12/pattern.json b/json/tests/draft2020-12/pattern.json index af0b8d8..af0b8d8 100644 --- a/tests/draft2020-12/pattern.json +++ b/json/tests/draft2020-12/pattern.json diff --git a/tests/draft2020-12/patternProperties.json b/json/tests/draft2020-12/patternProperties.json index 81829c7..81829c7 100644 --- a/tests/draft2020-12/patternProperties.json +++ b/json/tests/draft2020-12/patternProperties.json diff --git a/tests/draft2020-12/prefixItems.json b/json/tests/draft2020-12/prefixItems.json index 0adfc06..0adfc06 100644 --- a/tests/draft2020-12/prefixItems.json +++ b/json/tests/draft2020-12/prefixItems.json diff --git a/tests/draft2020-12/properties.json b/json/tests/draft2020-12/properties.json index eb66fa8..eb66fa8 100644 --- a/tests/draft2020-12/properties.json +++ b/json/tests/draft2020-12/properties.json diff --git a/tests/draft2020-12/propertyNames.json b/json/tests/draft2020-12/propertyNames.json index 7ecfb7e..7ecfb7e 100644 --- a/tests/draft2020-12/propertyNames.json +++ b/json/tests/draft2020-12/propertyNames.json diff --git a/tests/draft2020-12/ref.json b/json/tests/draft2020-12/ref.json index 72c593a..72c593a 100644 --- a/tests/draft2020-12/ref.json +++ b/json/tests/draft2020-12/ref.json diff --git a/tests/draft2020-12/refRemote.json b/json/tests/draft2020-12/refRemote.json index 2d4f99e..2d4f99e 100644 --- a/tests/draft2020-12/refRemote.json +++ b/json/tests/draft2020-12/refRemote.json diff --git a/tests/draft2020-12/required.json b/json/tests/draft2020-12/required.json index b7cb99a..b7cb99a 100644 --- a/tests/draft2020-12/required.json +++ b/json/tests/draft2020-12/required.json diff --git a/tests/draft2020-12/type.json b/json/tests/draft2020-12/type.json index 2123c40..2123c40 100644 --- a/tests/draft2020-12/type.json +++ b/json/tests/draft2020-12/type.json diff --git a/tests/draft2020-12/unevaluatedItems.json b/json/tests/draft2020-12/unevaluatedItems.json index 2b816af..2b816af 100644 --- a/tests/draft2020-12/unevaluatedItems.json +++ b/json/tests/draft2020-12/unevaluatedItems.json diff --git a/tests/draft2020-12/unevaluatedProperties.json b/json/tests/draft2020-12/unevaluatedProperties.json index 00cb36c..00cb36c 100644 --- a/tests/draft2020-12/unevaluatedProperties.json +++ b/json/tests/draft2020-12/unevaluatedProperties.json diff --git a/tests/draft2020-12/uniqueItems.json b/json/tests/draft2020-12/uniqueItems.json index 707235c..707235c 100644 --- a/tests/draft2020-12/uniqueItems.json +++ b/json/tests/draft2020-12/uniqueItems.json diff --git a/tests/draft2020-12/unknownKeyword.json b/json/tests/draft2020-12/unknownKeyword.json index 28b0c4c..28b0c4c 100644 --- a/tests/draft2020-12/unknownKeyword.json +++ b/json/tests/draft2020-12/unknownKeyword.json diff --git a/tests/draft2020-12/vocabulary.json b/json/tests/draft2020-12/vocabulary.json index 1acb96a..1acb96a 100644 --- a/tests/draft2020-12/vocabulary.json +++ b/json/tests/draft2020-12/vocabulary.json diff --git a/tests/draft3/additionalItems.json b/json/tests/draft3/additionalItems.json index 0cb6687..0cb6687 100644 --- a/tests/draft3/additionalItems.json +++ b/json/tests/draft3/additionalItems.json diff --git a/tests/draft3/additionalProperties.json b/json/tests/draft3/additionalProperties.json index af7bfc6..af7bfc6 100644 --- a/tests/draft3/additionalProperties.json +++ b/json/tests/draft3/additionalProperties.json diff --git a/tests/draft3/default.json b/json/tests/draft3/default.json index 289a9b6..289a9b6 100644 --- a/tests/draft3/default.json +++ b/json/tests/draft3/default.json diff --git a/tests/draft3/dependencies.json b/json/tests/draft3/dependencies.json index 0ffa6bf..0ffa6bf 100644 --- a/tests/draft3/dependencies.json +++ b/json/tests/draft3/dependencies.json diff --git a/tests/draft3/disallow.json b/json/tests/draft3/disallow.json index a5c9d90..a5c9d90 100644 --- a/tests/draft3/disallow.json +++ b/json/tests/draft3/disallow.json diff --git a/tests/draft3/divisibleBy.json b/json/tests/draft3/divisibleBy.json index ef7cc14..ef7cc14 100644 --- a/tests/draft3/divisibleBy.json +++ b/json/tests/draft3/divisibleBy.json diff --git a/tests/draft3/enum.json b/json/tests/draft3/enum.json index 5a1ab3b..5a1ab3b 100644 --- a/tests/draft3/enum.json +++ b/json/tests/draft3/enum.json diff --git a/tests/draft3/extends.json b/json/tests/draft3/extends.json index 909bce5..909bce5 100644 --- a/tests/draft3/extends.json +++ b/json/tests/draft3/extends.json diff --git a/tests/draft3/format.json b/json/tests/draft3/format.json index a5447c9..a5447c9 100644 --- a/tests/draft3/format.json +++ b/json/tests/draft3/format.json diff --git a/tests/draft3/infinite-loop-detection.json b/json/tests/draft3/infinite-loop-detection.json index 090f49a..090f49a 100644 --- a/tests/draft3/infinite-loop-detection.json +++ b/json/tests/draft3/infinite-loop-detection.json diff --git a/tests/draft3/items.json b/json/tests/draft3/items.json index e8bda22..e8bda22 100644 --- a/tests/draft3/items.json +++ b/json/tests/draft3/items.json diff --git a/tests/draft3/maxItems.json b/json/tests/draft3/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft3/maxItems.json +++ b/json/tests/draft3/maxItems.json diff --git a/tests/draft3/maxLength.json b/json/tests/draft3/maxLength.json index 4de42bc..4de42bc 100644 --- a/tests/draft3/maxLength.json +++ b/json/tests/draft3/maxLength.json diff --git a/tests/draft3/maximum.json b/json/tests/draft3/maximum.json index ccb79c6..ccb79c6 100644 --- a/tests/draft3/maximum.json +++ b/json/tests/draft3/maximum.json diff --git a/tests/draft3/minItems.json b/json/tests/draft3/minItems.json index ed51188..ed51188 100644 --- a/tests/draft3/minItems.json +++ b/json/tests/draft3/minItems.json diff --git a/tests/draft3/minLength.json b/json/tests/draft3/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft3/minLength.json +++ b/json/tests/draft3/minLength.json diff --git a/tests/draft3/minimum.json b/json/tests/draft3/minimum.json index d579536..d579536 100644 --- a/tests/draft3/minimum.json +++ b/json/tests/draft3/minimum.json diff --git a/tests/draft3/optional/bignum.json b/json/tests/draft3/optional/bignum.json index 1bc8eb2..1bc8eb2 100644 --- a/tests/draft3/optional/bignum.json +++ b/json/tests/draft3/optional/bignum.json diff --git a/tests/draft3/optional/ecmascript-regex.json b/json/tests/draft3/optional/ecmascript-regex.json index 03fe977..03fe977 100644 --- a/tests/draft3/optional/ecmascript-regex.json +++ b/json/tests/draft3/optional/ecmascript-regex.json diff --git a/tests/draft3/optional/format/color.json b/json/tests/draft3/optional/format/color.json index 0c0b534..0c0b534 100644 --- a/tests/draft3/optional/format/color.json +++ b/json/tests/draft3/optional/format/color.json diff --git a/tests/draft3/optional/format/date-time.json b/json/tests/draft3/optional/format/date-time.json index 1f1e6fb..1f1e6fb 100644 --- a/tests/draft3/optional/format/date-time.json +++ b/json/tests/draft3/optional/format/date-time.json diff --git a/tests/draft3/optional/format/date.json b/json/tests/draft3/optional/format/date.json index 796bc46..796bc46 100644 --- a/tests/draft3/optional/format/date.json +++ b/json/tests/draft3/optional/format/date.json diff --git a/tests/draft3/optional/format/email.json b/json/tests/draft3/optional/format/email.json index 059615a..059615a 100644 --- a/tests/draft3/optional/format/email.json +++ b/json/tests/draft3/optional/format/email.json diff --git a/tests/draft3/optional/format/host-name.json b/json/tests/draft3/optional/format/host-name.json index d418f37..d418f37 100644 --- a/tests/draft3/optional/format/host-name.json +++ b/json/tests/draft3/optional/format/host-name.json diff --git a/tests/draft3/optional/format/ip-address.json b/json/tests/draft3/optional/format/ip-address.json index 91cac9f..91cac9f 100644 --- a/tests/draft3/optional/format/ip-address.json +++ b/json/tests/draft3/optional/format/ip-address.json diff --git a/tests/draft3/optional/format/ipv6.json b/json/tests/draft3/optional/format/ipv6.json index c3ef379..c3ef379 100644 --- a/tests/draft3/optional/format/ipv6.json +++ b/json/tests/draft3/optional/format/ipv6.json diff --git a/tests/draft3/optional/format/regex.json b/json/tests/draft3/optional/format/regex.json index 8a37763..8a37763 100644 --- a/tests/draft3/optional/format/regex.json +++ b/json/tests/draft3/optional/format/regex.json diff --git a/tests/draft3/optional/format/time.json b/json/tests/draft3/optional/format/time.json index 36c823e..36c823e 100644 --- a/tests/draft3/optional/format/time.json +++ b/json/tests/draft3/optional/format/time.json diff --git a/tests/draft3/optional/format/uri.json b/json/tests/draft3/optional/format/uri.json index f024b62..f024b62 100644 --- a/tests/draft3/optional/format/uri.json +++ b/json/tests/draft3/optional/format/uri.json diff --git a/tests/draft3/optional/non-bmp-regex.json b/json/tests/draft3/optional/non-bmp-regex.json index dd67af2..dd67af2 100644 --- a/tests/draft3/optional/non-bmp-regex.json +++ b/json/tests/draft3/optional/non-bmp-regex.json diff --git a/tests/draft3/optional/zeroTerminatedFloats.json b/json/tests/draft3/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft3/optional/zeroTerminatedFloats.json +++ b/json/tests/draft3/optional/zeroTerminatedFloats.json diff --git a/tests/draft3/pattern.json b/json/tests/draft3/pattern.json index 92db0f9..92db0f9 100644 --- a/tests/draft3/pattern.json +++ b/json/tests/draft3/pattern.json diff --git a/tests/draft3/patternProperties.json b/json/tests/draft3/patternProperties.json index b0f2a8e..b0f2a8e 100644 --- a/tests/draft3/patternProperties.json +++ b/json/tests/draft3/patternProperties.json diff --git a/tests/draft3/properties.json b/json/tests/draft3/properties.json index cd23801..cd23801 100644 --- a/tests/draft3/properties.json +++ b/json/tests/draft3/properties.json diff --git a/tests/draft3/ref.json b/json/tests/draft3/ref.json index 609eaa4..609eaa4 100644 --- a/tests/draft3/ref.json +++ b/json/tests/draft3/ref.json diff --git a/tests/draft3/refRemote.json b/json/tests/draft3/refRemote.json index de0cb43..de0cb43 100644 --- a/tests/draft3/refRemote.json +++ b/json/tests/draft3/refRemote.json diff --git a/tests/draft3/required.json b/json/tests/draft3/required.json index aaaf024..aaaf024 100644 --- a/tests/draft3/required.json +++ b/json/tests/draft3/required.json diff --git a/tests/draft3/type.json b/json/tests/draft3/type.json index 8447bc8..8447bc8 100644 --- a/tests/draft3/type.json +++ b/json/tests/draft3/type.json diff --git a/tests/draft3/uniqueItems.json b/json/tests/draft3/uniqueItems.json index c48c6a0..c48c6a0 100644 --- a/tests/draft3/uniqueItems.json +++ b/json/tests/draft3/uniqueItems.json diff --git a/tests/draft4/additionalItems.json b/json/tests/draft4/additionalItems.json index deb44fd..deb44fd 100644 --- a/tests/draft4/additionalItems.json +++ b/json/tests/draft4/additionalItems.json diff --git a/tests/draft4/additionalProperties.json b/json/tests/draft4/additionalProperties.json index 0f8e162..0f8e162 100644 --- a/tests/draft4/additionalProperties.json +++ b/json/tests/draft4/additionalProperties.json diff --git a/tests/draft4/allOf.json b/json/tests/draft4/allOf.json index fc7dec5..fc7dec5 100644 --- a/tests/draft4/allOf.json +++ b/json/tests/draft4/allOf.json diff --git a/tests/draft4/anyOf.json b/json/tests/draft4/anyOf.json index 09cc3c2..09cc3c2 100644 --- a/tests/draft4/anyOf.json +++ b/json/tests/draft4/anyOf.json diff --git a/tests/draft4/default.json b/json/tests/draft4/default.json index 289a9b6..289a9b6 100644 --- a/tests/draft4/default.json +++ b/json/tests/draft4/default.json diff --git a/tests/draft4/definitions.json b/json/tests/draft4/definitions.json index 482823b..482823b 100644 --- a/tests/draft4/definitions.json +++ b/json/tests/draft4/definitions.json diff --git a/tests/draft4/dependencies.json b/json/tests/draft4/dependencies.json index 51eeddf..51eeddf 100644 --- a/tests/draft4/dependencies.json +++ b/json/tests/draft4/dependencies.json diff --git a/tests/draft4/enum.json b/json/tests/draft4/enum.json index f085097..f085097 100644 --- a/tests/draft4/enum.json +++ b/json/tests/draft4/enum.json diff --git a/tests/draft4/format.json b/json/tests/draft4/format.json index 5bd83cc..5bd83cc 100644 --- a/tests/draft4/format.json +++ b/json/tests/draft4/format.json diff --git a/tests/draft4/id.json b/json/tests/draft4/id.json index 1c91d33..1c91d33 100644 --- a/tests/draft4/id.json +++ b/json/tests/draft4/id.json diff --git a/tests/draft4/infinite-loop-detection.json b/json/tests/draft4/infinite-loop-detection.json index f98c74f..f98c74f 100644 --- a/tests/draft4/infinite-loop-detection.json +++ b/json/tests/draft4/infinite-loop-detection.json diff --git a/tests/draft4/items.json b/json/tests/draft4/items.json index 16ea070..16ea070 100644 --- a/tests/draft4/items.json +++ b/json/tests/draft4/items.json diff --git a/tests/draft4/maxItems.json b/json/tests/draft4/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft4/maxItems.json +++ b/json/tests/draft4/maxItems.json diff --git a/tests/draft4/maxLength.json b/json/tests/draft4/maxLength.json index 811d35b..811d35b 100644 --- a/tests/draft4/maxLength.json +++ b/json/tests/draft4/maxLength.json diff --git a/tests/draft4/maxProperties.json b/json/tests/draft4/maxProperties.json index aa7209f..aa7209f 100644 --- a/tests/draft4/maxProperties.json +++ b/json/tests/draft4/maxProperties.json diff --git a/tests/draft4/maximum.json b/json/tests/draft4/maximum.json index ccb79c6..ccb79c6 100644 --- a/tests/draft4/maximum.json +++ b/json/tests/draft4/maximum.json diff --git a/tests/draft4/minItems.json b/json/tests/draft4/minItems.json index ed51188..ed51188 100644 --- a/tests/draft4/minItems.json +++ b/json/tests/draft4/minItems.json diff --git a/tests/draft4/minLength.json b/json/tests/draft4/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft4/minLength.json +++ b/json/tests/draft4/minLength.json diff --git a/tests/draft4/minProperties.json b/json/tests/draft4/minProperties.json index 49a0726..49a0726 100644 --- a/tests/draft4/minProperties.json +++ b/json/tests/draft4/minProperties.json diff --git a/tests/draft4/minimum.json b/json/tests/draft4/minimum.json index 22d310e..22d310e 100644 --- a/tests/draft4/minimum.json +++ b/json/tests/draft4/minimum.json diff --git a/tests/draft4/multipleOf.json b/json/tests/draft4/multipleOf.json index ed2df4a..ed2df4a 100644 --- a/tests/draft4/multipleOf.json +++ b/json/tests/draft4/multipleOf.json diff --git a/tests/draft4/not.json b/json/tests/draft4/not.json index cbb7f46..cbb7f46 100644 --- a/tests/draft4/not.json +++ b/json/tests/draft4/not.json diff --git a/tests/draft4/oneOf.json b/json/tests/draft4/oneOf.json index fb63b08..fb63b08 100644 --- a/tests/draft4/oneOf.json +++ b/json/tests/draft4/oneOf.json diff --git a/tests/draft4/optional/bignum.json b/json/tests/draft4/optional/bignum.json index 1bc8eb2..1bc8eb2 100644 --- a/tests/draft4/optional/bignum.json +++ b/json/tests/draft4/optional/bignum.json diff --git a/tests/draft4/optional/ecmascript-regex.json b/json/tests/draft4/optional/ecmascript-regex.json index c431bac..c431bac 100644 --- a/tests/draft4/optional/ecmascript-regex.json +++ b/json/tests/draft4/optional/ecmascript-regex.json diff --git a/tests/draft4/optional/float-overflow.json b/json/tests/draft4/optional/float-overflow.json index 47fd5ba..47fd5ba 100644 --- a/tests/draft4/optional/float-overflow.json +++ b/json/tests/draft4/optional/float-overflow.json diff --git a/tests/draft4/optional/format/date-time.json b/json/tests/draft4/optional/format/date-time.json index 0911273..0911273 100644 --- a/tests/draft4/optional/format/date-time.json +++ b/json/tests/draft4/optional/format/date-time.json diff --git a/tests/draft4/optional/format/email.json b/json/tests/draft4/optional/format/email.json index d6761a4..d6761a4 100644 --- a/tests/draft4/optional/format/email.json +++ b/json/tests/draft4/optional/format/email.json diff --git a/tests/draft4/optional/format/hostname.json b/json/tests/draft4/optional/format/hostname.json index 8a67fda..8a67fda 100644 --- a/tests/draft4/optional/format/hostname.json +++ b/json/tests/draft4/optional/format/hostname.json diff --git a/tests/draft4/optional/format/ipv4.json b/json/tests/draft4/optional/format/ipv4.json index 4706581..4706581 100644 --- a/tests/draft4/optional/format/ipv4.json +++ b/json/tests/draft4/optional/format/ipv4.json diff --git a/tests/draft4/optional/format/ipv6.json b/json/tests/draft4/optional/format/ipv6.json index 94368f2..94368f2 100644 --- a/tests/draft4/optional/format/ipv6.json +++ b/json/tests/draft4/optional/format/ipv6.json diff --git a/tests/draft4/optional/format/unknown.json b/json/tests/draft4/optional/format/unknown.json index 12339ae..12339ae 100644 --- a/tests/draft4/optional/format/unknown.json +++ b/json/tests/draft4/optional/format/unknown.json diff --git a/tests/draft4/optional/format/uri.json b/json/tests/draft4/optional/format/uri.json index 4b48d40..4b48d40 100644 --- a/tests/draft4/optional/format/uri.json +++ b/json/tests/draft4/optional/format/uri.json diff --git a/tests/draft4/optional/non-bmp-regex.json b/json/tests/draft4/optional/non-bmp-regex.json index dd67af2..dd67af2 100644 --- a/tests/draft4/optional/non-bmp-regex.json +++ b/json/tests/draft4/optional/non-bmp-regex.json diff --git a/tests/draft4/optional/zeroTerminatedFloats.json b/json/tests/draft4/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft4/optional/zeroTerminatedFloats.json +++ b/json/tests/draft4/optional/zeroTerminatedFloats.json diff --git a/tests/draft4/pattern.json b/json/tests/draft4/pattern.json index 92db0f9..92db0f9 100644 --- a/tests/draft4/pattern.json +++ b/json/tests/draft4/pattern.json diff --git a/tests/draft4/patternProperties.json b/json/tests/draft4/patternProperties.json index 51c8af3..51c8af3 100644 --- a/tests/draft4/patternProperties.json +++ b/json/tests/draft4/patternProperties.json diff --git a/tests/draft4/properties.json b/json/tests/draft4/properties.json index 195159e..195159e 100644 --- a/tests/draft4/properties.json +++ b/json/tests/draft4/properties.json diff --git a/tests/draft4/ref.json b/json/tests/draft4/ref.json index b714fb0..b714fb0 100644 --- a/tests/draft4/ref.json +++ b/json/tests/draft4/ref.json diff --git a/tests/draft4/refRemote.json b/json/tests/draft4/refRemote.json index 412c9ff..412c9ff 100644 --- a/tests/draft4/refRemote.json +++ b/json/tests/draft4/refRemote.json diff --git a/tests/draft4/required.json b/json/tests/draft4/required.json index 6ccfdc2..6ccfdc2 100644 --- a/tests/draft4/required.json +++ b/json/tests/draft4/required.json diff --git a/tests/draft4/type.json b/json/tests/draft4/type.json index df46677..df46677 100644 --- a/tests/draft4/type.json +++ b/json/tests/draft4/type.json diff --git a/tests/draft4/uniqueItems.json b/json/tests/draft4/uniqueItems.json index 2ccf666..2ccf666 100644 --- a/tests/draft4/uniqueItems.json +++ b/json/tests/draft4/uniqueItems.json diff --git a/tests/draft6/additionalItems.json b/json/tests/draft6/additionalItems.json index cae7236..cae7236 100644 --- a/tests/draft6/additionalItems.json +++ b/json/tests/draft6/additionalItems.json diff --git a/tests/draft6/additionalProperties.json b/json/tests/draft6/additionalProperties.json index 0f8e162..0f8e162 100644 --- a/tests/draft6/additionalProperties.json +++ b/json/tests/draft6/additionalProperties.json diff --git a/tests/draft6/allOf.json b/json/tests/draft6/allOf.json index ec9319e..ec9319e 100644 --- a/tests/draft6/allOf.json +++ b/json/tests/draft6/allOf.json diff --git a/tests/draft6/anyOf.json b/json/tests/draft6/anyOf.json index ab5eb38..ab5eb38 100644 --- a/tests/draft6/anyOf.json +++ b/json/tests/draft6/anyOf.json diff --git a/tests/draft6/boolean_schema.json b/json/tests/draft6/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft6/boolean_schema.json +++ b/json/tests/draft6/boolean_schema.json diff --git a/tests/draft6/const.json b/json/tests/draft6/const.json index 1c2cafc..1c2cafc 100644 --- a/tests/draft6/const.json +++ b/json/tests/draft6/const.json diff --git a/tests/draft6/contains.json b/json/tests/draft6/contains.json index bd93654..bd93654 100644 --- a/tests/draft6/contains.json +++ b/json/tests/draft6/contains.json diff --git a/tests/draft6/default.json b/json/tests/draft6/default.json index 289a9b6..289a9b6 100644 --- a/tests/draft6/default.json +++ b/json/tests/draft6/default.json diff --git a/tests/draft6/definitions.json b/json/tests/draft6/definitions.json index d772fde..d772fde 100644 --- a/tests/draft6/definitions.json +++ b/json/tests/draft6/definitions.json diff --git a/tests/draft6/dependencies.json b/json/tests/draft6/dependencies.json index a5e5428..a5e5428 100644 --- a/tests/draft6/dependencies.json +++ b/json/tests/draft6/dependencies.json diff --git a/tests/draft6/enum.json b/json/tests/draft6/enum.json index f085097..f085097 100644 --- a/tests/draft6/enum.json +++ b/json/tests/draft6/enum.json diff --git a/tests/draft6/exclusiveMaximum.json b/json/tests/draft6/exclusiveMaximum.json index dc3cd70..dc3cd70 100644 --- a/tests/draft6/exclusiveMaximum.json +++ b/json/tests/draft6/exclusiveMaximum.json diff --git a/tests/draft6/exclusiveMinimum.json b/json/tests/draft6/exclusiveMinimum.json index b38d7ec..b38d7ec 100644 --- a/tests/draft6/exclusiveMinimum.json +++ b/json/tests/draft6/exclusiveMinimum.json diff --git a/tests/draft6/format.json b/json/tests/draft6/format.json index 2df2a9f..2df2a9f 100644 --- a/tests/draft6/format.json +++ b/json/tests/draft6/format.json diff --git a/tests/draft6/id.json b/json/tests/draft6/id.json index 03d30fc..03d30fc 100644 --- a/tests/draft6/id.json +++ b/json/tests/draft6/id.json diff --git a/tests/draft6/infinite-loop-detection.json b/json/tests/draft6/infinite-loop-detection.json index f98c74f..f98c74f 100644 --- a/tests/draft6/infinite-loop-detection.json +++ b/json/tests/draft6/infinite-loop-detection.json diff --git a/tests/draft6/items.json b/json/tests/draft6/items.json index 7ed6781..7ed6781 100644 --- a/tests/draft6/items.json +++ b/json/tests/draft6/items.json diff --git a/tests/draft6/maxItems.json b/json/tests/draft6/maxItems.json index f0c36ab..f0c36ab 100644 --- a/tests/draft6/maxItems.json +++ b/json/tests/draft6/maxItems.json diff --git a/tests/draft6/maxLength.json b/json/tests/draft6/maxLength.json index 748b4da..748b4da 100644 --- a/tests/draft6/maxLength.json +++ b/json/tests/draft6/maxLength.json diff --git a/tests/draft6/maxProperties.json b/json/tests/draft6/maxProperties.json index acec142..acec142 100644 --- a/tests/draft6/maxProperties.json +++ b/json/tests/draft6/maxProperties.json diff --git a/tests/draft6/maximum.json b/json/tests/draft6/maximum.json index 6844a39..6844a39 100644 --- a/tests/draft6/maximum.json +++ b/json/tests/draft6/maximum.json diff --git a/tests/draft6/minItems.json b/json/tests/draft6/minItems.json index d3b1872..d3b1872 100644 --- a/tests/draft6/minItems.json +++ b/json/tests/draft6/minItems.json diff --git a/tests/draft6/minLength.json b/json/tests/draft6/minLength.json index 64db948..64db948 100644 --- a/tests/draft6/minLength.json +++ b/json/tests/draft6/minLength.json diff --git a/tests/draft6/minProperties.json b/json/tests/draft6/minProperties.json index 9f74f78..9f74f78 100644 --- a/tests/draft6/minProperties.json +++ b/json/tests/draft6/minProperties.json diff --git a/tests/draft6/minimum.json b/json/tests/draft6/minimum.json index 21ae50e..21ae50e 100644 --- a/tests/draft6/minimum.json +++ b/json/tests/draft6/minimum.json diff --git a/tests/draft6/multipleOf.json b/json/tests/draft6/multipleOf.json index e606979..e606979 100644 --- a/tests/draft6/multipleOf.json +++ b/json/tests/draft6/multipleOf.json diff --git a/tests/draft6/not.json b/json/tests/draft6/not.json index 98de0ed..98de0ed 100644 --- a/tests/draft6/not.json +++ b/json/tests/draft6/not.json diff --git a/tests/draft6/oneOf.json b/json/tests/draft6/oneOf.json index eeb7ae8..eeb7ae8 100644 --- a/tests/draft6/oneOf.json +++ b/json/tests/draft6/oneOf.json diff --git a/tests/draft6/optional/bignum.json b/json/tests/draft6/optional/bignum.json index 94b4a4e..94b4a4e 100644 --- a/tests/draft6/optional/bignum.json +++ b/json/tests/draft6/optional/bignum.json diff --git a/tests/draft6/optional/ecmascript-regex.json b/json/tests/draft6/optional/ecmascript-regex.json index c4886aa..c4886aa 100644 --- a/tests/draft6/optional/ecmascript-regex.json +++ b/json/tests/draft6/optional/ecmascript-regex.json diff --git a/tests/draft6/optional/float-overflow.json b/json/tests/draft6/optional/float-overflow.json index 52ff982..52ff982 100644 --- a/tests/draft6/optional/float-overflow.json +++ b/json/tests/draft6/optional/float-overflow.json diff --git a/tests/draft6/optional/format/date-time.json b/json/tests/draft6/optional/format/date-time.json index 0911273..0911273 100644 --- a/tests/draft6/optional/format/date-time.json +++ b/json/tests/draft6/optional/format/date-time.json diff --git a/tests/draft6/optional/format/email.json b/json/tests/draft6/optional/format/email.json index d6761a4..d6761a4 100644 --- a/tests/draft6/optional/format/email.json +++ b/json/tests/draft6/optional/format/email.json diff --git a/tests/draft6/optional/format/hostname.json b/json/tests/draft6/optional/format/hostname.json index 8a67fda..8a67fda 100644 --- a/tests/draft6/optional/format/hostname.json +++ b/json/tests/draft6/optional/format/hostname.json diff --git a/tests/draft6/optional/format/ipv4.json b/json/tests/draft6/optional/format/ipv4.json index 4706581..4706581 100644 --- a/tests/draft6/optional/format/ipv4.json +++ b/json/tests/draft6/optional/format/ipv4.json diff --git a/tests/draft6/optional/format/ipv6.json b/json/tests/draft6/optional/format/ipv6.json index 94368f2..94368f2 100644 --- a/tests/draft6/optional/format/ipv6.json +++ b/json/tests/draft6/optional/format/ipv6.json diff --git a/tests/draft6/optional/format/json-pointer.json b/json/tests/draft6/optional/format/json-pointer.json index a0346b5..a0346b5 100644 --- a/tests/draft6/optional/format/json-pointer.json +++ b/json/tests/draft6/optional/format/json-pointer.json diff --git a/tests/draft6/optional/format/unknown.json b/json/tests/draft6/optional/format/unknown.json index 12339ae..12339ae 100644 --- a/tests/draft6/optional/format/unknown.json +++ b/json/tests/draft6/optional/format/unknown.json diff --git a/tests/draft6/optional/format/uri-reference.json b/json/tests/draft6/optional/format/uri-reference.json index 7cdf228..7cdf228 100644 --- a/tests/draft6/optional/format/uri-reference.json +++ b/json/tests/draft6/optional/format/uri-reference.json diff --git a/tests/draft6/optional/format/uri-template.json b/json/tests/draft6/optional/format/uri-template.json index df355c5..df355c5 100644 --- a/tests/draft6/optional/format/uri-template.json +++ b/json/tests/draft6/optional/format/uri-template.json diff --git a/tests/draft6/optional/format/uri.json b/json/tests/draft6/optional/format/uri.json index 4b48d40..4b48d40 100644 --- a/tests/draft6/optional/format/uri.json +++ b/json/tests/draft6/optional/format/uri.json diff --git a/tests/draft6/optional/non-bmp-regex.json b/json/tests/draft6/optional/non-bmp-regex.json index dd67af2..dd67af2 100644 --- a/tests/draft6/optional/non-bmp-regex.json +++ b/json/tests/draft6/optional/non-bmp-regex.json diff --git a/tests/draft6/pattern.json b/json/tests/draft6/pattern.json index 92db0f9..92db0f9 100644 --- a/tests/draft6/pattern.json +++ b/json/tests/draft6/pattern.json diff --git a/tests/draft6/patternProperties.json b/json/tests/draft6/patternProperties.json index c276e64..c276e64 100644 --- a/tests/draft6/patternProperties.json +++ b/json/tests/draft6/patternProperties.json diff --git a/tests/draft6/properties.json b/json/tests/draft6/properties.json index 5b971ca..5b971ca 100644 --- a/tests/draft6/properties.json +++ b/json/tests/draft6/properties.json diff --git a/tests/draft6/propertyNames.json b/json/tests/draft6/propertyNames.json index f0788e6..f0788e6 100644 --- a/tests/draft6/propertyNames.json +++ b/json/tests/draft6/propertyNames.json diff --git a/tests/draft6/ref.json b/json/tests/draft6/ref.json index 8a36593..8a36593 100644 --- a/tests/draft6/ref.json +++ b/json/tests/draft6/ref.json diff --git a/tests/draft6/refRemote.json b/json/tests/draft6/refRemote.json index c2b2002..c2b2002 100644 --- a/tests/draft6/refRemote.json +++ b/json/tests/draft6/refRemote.json diff --git a/tests/draft6/required.json b/json/tests/draft6/required.json index 8d8087a..8d8087a 100644 --- a/tests/draft6/required.json +++ b/json/tests/draft6/required.json diff --git a/tests/draft6/type.json b/json/tests/draft6/type.json index 8304647..8304647 100644 --- a/tests/draft6/type.json +++ b/json/tests/draft6/type.json diff --git a/tests/draft6/uniqueItems.json b/json/tests/draft6/uniqueItems.json index 2ccf666..2ccf666 100644 --- a/tests/draft6/uniqueItems.json +++ b/json/tests/draft6/uniqueItems.json diff --git a/tests/draft6/unknownKeyword.json b/json/tests/draft6/unknownKeyword.json index 1f58d97..1f58d97 100644 --- a/tests/draft6/unknownKeyword.json +++ b/json/tests/draft6/unknownKeyword.json diff --git a/tests/draft7/additionalItems.json b/json/tests/draft7/additionalItems.json index cae7236..cae7236 100644 --- a/tests/draft7/additionalItems.json +++ b/json/tests/draft7/additionalItems.json diff --git a/tests/draft7/additionalProperties.json b/json/tests/draft7/additionalProperties.json index 0f8e162..0f8e162 100644 --- a/tests/draft7/additionalProperties.json +++ b/json/tests/draft7/additionalProperties.json diff --git a/tests/draft7/allOf.json b/json/tests/draft7/allOf.json index ec9319e..ec9319e 100644 --- a/tests/draft7/allOf.json +++ b/json/tests/draft7/allOf.json diff --git a/tests/draft7/anyOf.json b/json/tests/draft7/anyOf.json index ab5eb38..ab5eb38 100644 --- a/tests/draft7/anyOf.json +++ b/json/tests/draft7/anyOf.json diff --git a/tests/draft7/boolean_schema.json b/json/tests/draft7/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft7/boolean_schema.json +++ b/json/tests/draft7/boolean_schema.json diff --git a/tests/draft7/const.json b/json/tests/draft7/const.json index 1c2cafc..1c2cafc 100644 --- a/tests/draft7/const.json +++ b/json/tests/draft7/const.json diff --git a/tests/draft7/contains.json b/json/tests/draft7/contains.json index 2b1a515..2b1a515 100644 --- a/tests/draft7/contains.json +++ b/json/tests/draft7/contains.json diff --git a/tests/draft7/default.json b/json/tests/draft7/default.json index 289a9b6..289a9b6 100644 --- a/tests/draft7/default.json +++ b/json/tests/draft7/default.json diff --git a/tests/draft7/definitions.json b/json/tests/draft7/definitions.json index afe396e..afe396e 100644 --- a/tests/draft7/definitions.json +++ b/json/tests/draft7/definitions.json diff --git a/tests/draft7/dependencies.json b/json/tests/draft7/dependencies.json index a5e5428..a5e5428 100644 --- a/tests/draft7/dependencies.json +++ b/json/tests/draft7/dependencies.json diff --git a/tests/draft7/enum.json b/json/tests/draft7/enum.json index f085097..f085097 100644 --- a/tests/draft7/enum.json +++ b/json/tests/draft7/enum.json diff --git a/tests/draft7/exclusiveMaximum.json b/json/tests/draft7/exclusiveMaximum.json index dc3cd70..dc3cd70 100644 --- a/tests/draft7/exclusiveMaximum.json +++ b/json/tests/draft7/exclusiveMaximum.json diff --git a/tests/draft7/exclusiveMinimum.json b/json/tests/draft7/exclusiveMinimum.json index b38d7ec..b38d7ec 100644 --- a/tests/draft7/exclusiveMinimum.json +++ b/json/tests/draft7/exclusiveMinimum.json diff --git a/tests/draft7/format.json b/json/tests/draft7/format.json index e2447d6..e2447d6 100644 --- a/tests/draft7/format.json +++ b/json/tests/draft7/format.json diff --git a/tests/draft7/id.json b/json/tests/draft7/id.json index 6be81b8..6be81b8 100644 --- a/tests/draft7/id.json +++ b/json/tests/draft7/id.json diff --git a/tests/draft7/if-then-else.json b/json/tests/draft7/if-then-else.json index 284e919..284e919 100644 --- a/tests/draft7/if-then-else.json +++ b/json/tests/draft7/if-then-else.json diff --git a/tests/draft7/infinite-loop-detection.json b/json/tests/draft7/infinite-loop-detection.json index f98c74f..f98c74f 100644 --- a/tests/draft7/infinite-loop-detection.json +++ b/json/tests/draft7/infinite-loop-detection.json diff --git a/tests/draft7/items.json b/json/tests/draft7/items.json index 7ed6781..7ed6781 100644 --- a/tests/draft7/items.json +++ b/json/tests/draft7/items.json diff --git a/tests/draft7/maxItems.json b/json/tests/draft7/maxItems.json index f0c36ab..f0c36ab 100644 --- a/tests/draft7/maxItems.json +++ b/json/tests/draft7/maxItems.json diff --git a/tests/draft7/maxLength.json b/json/tests/draft7/maxLength.json index 748b4da..748b4da 100644 --- a/tests/draft7/maxLength.json +++ b/json/tests/draft7/maxLength.json diff --git a/tests/draft7/maxProperties.json b/json/tests/draft7/maxProperties.json index acec142..acec142 100644 --- a/tests/draft7/maxProperties.json +++ b/json/tests/draft7/maxProperties.json diff --git a/tests/draft7/maximum.json b/json/tests/draft7/maximum.json index 6844a39..6844a39 100644 --- a/tests/draft7/maximum.json +++ b/json/tests/draft7/maximum.json diff --git a/tests/draft7/minItems.json b/json/tests/draft7/minItems.json index d3b1872..d3b1872 100644 --- a/tests/draft7/minItems.json +++ b/json/tests/draft7/minItems.json diff --git a/tests/draft7/minLength.json b/json/tests/draft7/minLength.json index 64db948..64db948 100644 --- a/tests/draft7/minLength.json +++ b/json/tests/draft7/minLength.json diff --git a/tests/draft7/minProperties.json b/json/tests/draft7/minProperties.json index 9f74f78..9f74f78 100644 --- a/tests/draft7/minProperties.json +++ b/json/tests/draft7/minProperties.json diff --git a/tests/draft7/minimum.json b/json/tests/draft7/minimum.json index 21ae50e..21ae50e 100644 --- a/tests/draft7/minimum.json +++ b/json/tests/draft7/minimum.json diff --git a/tests/draft7/multipleOf.json b/json/tests/draft7/multipleOf.json index e606979..e606979 100644 --- a/tests/draft7/multipleOf.json +++ b/json/tests/draft7/multipleOf.json diff --git a/tests/draft7/not.json b/json/tests/draft7/not.json index 98de0ed..98de0ed 100644 --- a/tests/draft7/not.json +++ b/json/tests/draft7/not.json diff --git a/tests/draft7/oneOf.json b/json/tests/draft7/oneOf.json index eeb7ae8..eeb7ae8 100644 --- a/tests/draft7/oneOf.json +++ b/json/tests/draft7/oneOf.json diff --git a/tests/draft7/optional/bignum.json b/json/tests/draft7/optional/bignum.json index 94b4a4e..94b4a4e 100644 --- a/tests/draft7/optional/bignum.json +++ b/json/tests/draft7/optional/bignum.json diff --git a/tests/draft7/optional/content.json b/json/tests/draft7/optional/content.json index 3f5a743..3f5a743 100644 --- a/tests/draft7/optional/content.json +++ b/json/tests/draft7/optional/content.json diff --git a/tests/draft7/optional/cross-draft.json b/json/tests/draft7/optional/cross-draft.json index 8ff5373..8ff5373 100644 --- a/tests/draft7/optional/cross-draft.json +++ b/json/tests/draft7/optional/cross-draft.json diff --git a/tests/draft7/optional/ecmascript-regex.json b/json/tests/draft7/optional/ecmascript-regex.json index c4886aa..c4886aa 100644 --- a/tests/draft7/optional/ecmascript-regex.json +++ b/json/tests/draft7/optional/ecmascript-regex.json diff --git a/tests/draft7/optional/float-overflow.json b/json/tests/draft7/optional/float-overflow.json index 52ff982..52ff982 100644 --- a/tests/draft7/optional/float-overflow.json +++ b/json/tests/draft7/optional/float-overflow.json diff --git a/tests/draft7/optional/format/date-time.json b/json/tests/draft7/optional/format/date-time.json index 0911273..0911273 100644 --- a/tests/draft7/optional/format/date-time.json +++ b/json/tests/draft7/optional/format/date-time.json diff --git a/tests/draft7/optional/format/date.json b/json/tests/draft7/optional/format/date.json index d723124..d723124 100644 --- a/tests/draft7/optional/format/date.json +++ b/json/tests/draft7/optional/format/date.json diff --git a/tests/draft7/optional/format/email.json b/json/tests/draft7/optional/format/email.json index d6761a4..d6761a4 100644 --- a/tests/draft7/optional/format/email.json +++ b/json/tests/draft7/optional/format/email.json diff --git a/tests/draft7/optional/format/hostname.json b/json/tests/draft7/optional/format/hostname.json index 8a67fda..8a67fda 100644 --- a/tests/draft7/optional/format/hostname.json +++ b/json/tests/draft7/optional/format/hostname.json diff --git a/tests/draft7/optional/format/idn-email.json b/json/tests/draft7/optional/format/idn-email.json index 6e21374..6e21374 100644 --- a/tests/draft7/optional/format/idn-email.json +++ b/json/tests/draft7/optional/format/idn-email.json diff --git a/tests/draft7/optional/format/idn-hostname.json b/json/tests/draft7/optional/format/idn-hostname.json index 6c8f86a..6c8f86a 100644 --- a/tests/draft7/optional/format/idn-hostname.json +++ b/json/tests/draft7/optional/format/idn-hostname.json diff --git a/tests/draft7/optional/format/ipv4.json b/json/tests/draft7/optional/format/ipv4.json index 4706581..4706581 100644 --- a/tests/draft7/optional/format/ipv4.json +++ b/json/tests/draft7/optional/format/ipv4.json diff --git a/tests/draft7/optional/format/ipv6.json b/json/tests/draft7/optional/format/ipv6.json index 94368f2..94368f2 100644 --- a/tests/draft7/optional/format/ipv6.json +++ b/json/tests/draft7/optional/format/ipv6.json diff --git a/tests/draft7/optional/format/iri-reference.json b/json/tests/draft7/optional/format/iri-reference.json index c6b4c22..c6b4c22 100644 --- a/tests/draft7/optional/format/iri-reference.json +++ b/json/tests/draft7/optional/format/iri-reference.json diff --git a/tests/draft7/optional/format/iri.json b/json/tests/draft7/optional/format/iri.json index a0d12ae..a0d12ae 100644 --- a/tests/draft7/optional/format/iri.json +++ b/json/tests/draft7/optional/format/iri.json diff --git a/tests/draft7/optional/format/json-pointer.json b/json/tests/draft7/optional/format/json-pointer.json index a0346b5..a0346b5 100644 --- a/tests/draft7/optional/format/json-pointer.json +++ b/json/tests/draft7/optional/format/json-pointer.json diff --git a/tests/draft7/optional/format/regex.json b/json/tests/draft7/optional/format/regex.json index 3449177..3449177 100644 --- a/tests/draft7/optional/format/regex.json +++ b/json/tests/draft7/optional/format/regex.json diff --git a/tests/draft7/optional/format/relative-json-pointer.json b/json/tests/draft7/optional/format/relative-json-pointer.json index e50e505..e50e505 100644 --- a/tests/draft7/optional/format/relative-json-pointer.json +++ b/json/tests/draft7/optional/format/relative-json-pointer.json diff --git a/tests/draft7/optional/format/time.json b/json/tests/draft7/optional/format/time.json index 014ecd8..014ecd8 100644 --- a/tests/draft7/optional/format/time.json +++ b/json/tests/draft7/optional/format/time.json diff --git a/tests/draft7/optional/format/unknown.json b/json/tests/draft7/optional/format/unknown.json index 12339ae..12339ae 100644 --- a/tests/draft7/optional/format/unknown.json +++ b/json/tests/draft7/optional/format/unknown.json diff --git a/tests/draft7/optional/format/uri-reference.json b/json/tests/draft7/optional/format/uri-reference.json index 7cdf228..7cdf228 100644 --- a/tests/draft7/optional/format/uri-reference.json +++ b/json/tests/draft7/optional/format/uri-reference.json diff --git a/tests/draft7/optional/format/uri-template.json b/json/tests/draft7/optional/format/uri-template.json index df355c5..df355c5 100644 --- a/tests/draft7/optional/format/uri-template.json +++ b/json/tests/draft7/optional/format/uri-template.json diff --git a/tests/draft7/optional/format/uri.json b/json/tests/draft7/optional/format/uri.json index 4b48d40..4b48d40 100644 --- a/tests/draft7/optional/format/uri.json +++ b/json/tests/draft7/optional/format/uri.json diff --git a/tests/draft7/optional/non-bmp-regex.json b/json/tests/draft7/optional/non-bmp-regex.json index dd67af2..dd67af2 100644 --- a/tests/draft7/optional/non-bmp-regex.json +++ b/json/tests/draft7/optional/non-bmp-regex.json diff --git a/tests/draft7/pattern.json b/json/tests/draft7/pattern.json index 92db0f9..92db0f9 100644 --- a/tests/draft7/pattern.json +++ b/json/tests/draft7/pattern.json diff --git a/tests/draft7/patternProperties.json b/json/tests/draft7/patternProperties.json index c276e64..c276e64 100644 --- a/tests/draft7/patternProperties.json +++ b/json/tests/draft7/patternProperties.json diff --git a/tests/draft7/properties.json b/json/tests/draft7/properties.json index 5b971ca..5b971ca 100644 --- a/tests/draft7/properties.json +++ b/json/tests/draft7/properties.json diff --git a/tests/draft7/propertyNames.json b/json/tests/draft7/propertyNames.json index f0788e6..f0788e6 100644 --- a/tests/draft7/propertyNames.json +++ b/json/tests/draft7/propertyNames.json diff --git a/tests/draft7/ref.json b/json/tests/draft7/ref.json index 2bee83e..2bee83e 100644 --- a/tests/draft7/ref.json +++ b/json/tests/draft7/ref.json diff --git a/tests/draft7/refRemote.json b/json/tests/draft7/refRemote.json index c2b2002..c2b2002 100644 --- a/tests/draft7/refRemote.json +++ b/json/tests/draft7/refRemote.json diff --git a/tests/draft7/required.json b/json/tests/draft7/required.json index 8d8087a..8d8087a 100644 --- a/tests/draft7/required.json +++ b/json/tests/draft7/required.json diff --git a/tests/draft7/type.json b/json/tests/draft7/type.json index 8304647..8304647 100644 --- a/tests/draft7/type.json +++ b/json/tests/draft7/type.json diff --git a/tests/draft7/uniqueItems.json b/json/tests/draft7/uniqueItems.json index 2ccf666..2ccf666 100644 --- a/tests/draft7/uniqueItems.json +++ b/json/tests/draft7/uniqueItems.json diff --git a/tests/draft7/unknownKeyword.json b/json/tests/draft7/unknownKeyword.json index 1f58d97..1f58d97 100644 --- a/tests/draft7/unknownKeyword.json +++ b/json/tests/draft7/unknownKeyword.json diff --git a/tests/latest b/json/tests/latest index 9a4784d..9a4784d 120000 --- a/tests/latest +++ b/json/tests/latest diff --git a/json/tox.ini b/json/tox.ini new file mode 100644 index 0000000..7ca9de9 --- /dev/null +++ b/json/tox.ini @@ -0,0 +1,9 @@ +[tox] +minversion = 1.6 +envlist = sanity +skipsdist = True + +[testenv:sanity] +# used just for validating the structure of the test case files themselves +deps = jsonschema==4.17.3 +commands = {envpython} bin/jsonschema_suite check diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py new file mode 100644 index 0000000..efa59f4 --- /dev/null +++ b/jsonschema/__init__.py @@ -0,0 +1,116 @@ +""" +An implementation of JSON Schema for Python. + +The main functionality is provided by the validator classes for each of the +supported JSON Schema versions. + +Most commonly, `jsonschema.validators.validate` is the quickest way to simply +validate a given instance under a schema, and will create a validator +for you. +""" +import warnings + +from jsonschema._format import FormatChecker +from jsonschema._types import TypeChecker +from jsonschema.exceptions import SchemaError, ValidationError +from jsonschema.protocols import Validator +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, + validate, +) + + +def __getattr__(name): + if name == "__version__": + warnings.warn( + "Accessing jsonschema.__version__ is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for jsonschema's version.", + DeprecationWarning, + stacklevel=2, + ) + + try: + from importlib import metadata + except ImportError: + import importlib_metadata as metadata + + return metadata.version("jsonschema") + elif name == "RefResolver": + from jsonschema.validators import _RefResolver + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver + elif name == "ErrorTree": + warnings.warn( + "Importing ErrorTree directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import ErrorTree + return ErrorTree + elif name == "FormatError": + warnings.warn( + "Importing FormatError directly from the jsonschema package " + "is deprecated and will become an ImportError. Import it from " + "jsonschema.exceptions instead.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import FormatError + return FormatError + elif name == "RefResolutionError": + from jsonschema.exceptions import _RefResolutionError + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError + + format_checkers = { + "draft3_format_checker": Draft3Validator, + "draft4_format_checker": Draft4Validator, + "draft6_format_checker": Draft6Validator, + "draft7_format_checker": Draft7Validator, + "draft201909_format_checker": Draft201909Validator, + "draft202012_format_checker": Draft202012Validator, + } + ValidatorForFormat = format_checkers.get(name) + if ValidatorForFormat is not None: + warnings.warn( + f"Accessing jsonschema.{name} is deprecated and will be " + "removed in a future release. Instead, use the FORMAT_CHECKER " + "attribute on the corresponding Validator.", + DeprecationWarning, + stacklevel=2, + ) + return ValidatorForFormat.FORMAT_CHECKER + + raise AttributeError(f"module {__name__} has no attribute {name}") + + +__all__ = [ + "Draft201909Validator", + "Draft202012Validator", + "Draft3Validator", + "Draft4Validator", + "Draft6Validator", + "Draft7Validator", + "FormatChecker", + "SchemaError", + "TypeChecker", + "ValidationError", + "Validator", + "validate", +] diff --git a/jsonschema/__main__.py b/jsonschema/__main__.py new file mode 100644 index 0000000..fb260ae --- /dev/null +++ b/jsonschema/__main__.py @@ -0,0 +1,6 @@ +""" +The jsonschema CLI is now deprecated in favor of check-jsonschema. +""" +from jsonschema.cli import main + +main() diff --git a/jsonschema/_format.py b/jsonschema/_format.py new file mode 100644 index 0000000..2231f5e --- /dev/null +++ b/jsonschema/_format.py @@ -0,0 +1,522 @@ +from __future__ import annotations + +from contextlib import suppress +from uuid import UUID +import datetime +import ipaddress +import re +import typing +import warnings + +from jsonschema.exceptions import FormatError + +_FormatCheckCallable = typing.Callable[[object], bool] +#: A format checker callable. +_F = typing.TypeVar("_F", bound=_FormatCheckCallable) +_RaisesType = typing.Union[ + typing.Type[Exception], typing.Tuple[typing.Type[Exception], ...], +] + +_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII) + + +class FormatChecker: + """ + A ``format`` property checker. + + JSON Schema does not mandate that the ``format`` property actually do any + validation. If validation is desired however, instances of this class can + be hooked into validators to enable format validation. + + `FormatChecker` objects always return ``True`` when asked about + formats that they do not know how to validate. + + To add a check for a custom format use the `FormatChecker.checks` + decorator. + + Arguments: + + formats: + + The known formats to validate. This argument can be used to + limit which formats will be used during validation. + """ + + checkers: dict[ + str, + tuple[_FormatCheckCallable, _RaisesType], + ] = {} + + def __init__(self, formats: typing.Iterable[str] | None = None): + if formats is None: + formats = self.checkers.keys() + self.checkers = {k: self.checkers[k] for k in formats} + + def __repr__(self): + return f"<FormatChecker checkers={sorted(self.checkers)}>" + + def checks( # noqa: D417,D214,D405 (charliermarsh/ruff#3547) + self, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + """ + Register a decorated function as validating a new format. + + Arguments: + + format: + + The format that the decorated function will check. + + raises: + + The exception(s) raised by the decorated function when an + invalid instance is found. + + The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute of the + resulting validation error. + """ # noqa: D417,D214,D405 (charliermarsh/ruff#3547) + + def _checks(func: _F) -> _F: + self.checkers[format] = (func, raises) + return func + + return _checks + + @classmethod + def cls_checks( + cls, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + warnings.warn( + ( + "FormatChecker.cls_checks is deprecated. Call " + "FormatChecker.checks on a specific FormatChecker instance " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return cls._cls_checks(format=format, raises=raises) + + @classmethod + def _cls_checks( + cls, format: str, raises: _RaisesType = (), + ) -> typing.Callable[[_F], _F]: + def _checks(func: _F) -> _F: + cls.checkers[format] = (func, raises) + return func + + return _checks + + def check(self, instance: object, format: str) -> None: + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format: + + The format that instance should conform to + + Raises: + + FormatError: + + if the instance does not conform to ``format`` + """ + if format not in self.checkers: + return + + func, raises = self.checkers[format] + result, cause = None, None + try: + result = func(instance) + except raises as e: + cause = e + if not result: + raise FormatError(f"{instance!r} is not a {format!r}", cause=cause) + + def conforms(self, instance: object, format: str) -> bool: + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format: + + The format that instance should conform to + + Returns: + + bool: whether it conformed + """ + try: + self.check(instance, format) + except FormatError: + return False + else: + return True + + +draft3_format_checker = FormatChecker() +draft4_format_checker = FormatChecker() +draft6_format_checker = FormatChecker() +draft7_format_checker = FormatChecker() +draft201909_format_checker = FormatChecker() +draft202012_format_checker = FormatChecker() + +_draft_checkers: dict[str, FormatChecker] = dict( + draft3=draft3_format_checker, + draft4=draft4_format_checker, + draft6=draft6_format_checker, + draft7=draft7_format_checker, + draft201909=draft201909_format_checker, + draft202012=draft202012_format_checker, +) + + +def _checks_drafts( + name=None, + draft3=None, + draft4=None, + draft6=None, + draft7=None, + draft201909=None, + draft202012=None, + raises=(), +) -> typing.Callable[[_F], _F]: + draft3 = draft3 or name + draft4 = draft4 or name + draft6 = draft6 or name + draft7 = draft7 or name + draft201909 = draft201909 or name + draft202012 = draft202012 or name + + def wrap(func: _F) -> _F: + if draft3: + func = _draft_checkers["draft3"].checks(draft3, raises)(func) + if draft4: + func = _draft_checkers["draft4"].checks(draft4, raises)(func) + if draft6: + func = _draft_checkers["draft6"].checks(draft6, raises)(func) + if draft7: + func = _draft_checkers["draft7"].checks(draft7, raises)(func) + if draft201909: + func = _draft_checkers["draft201909"].checks(draft201909, raises)( + func, + ) + if draft202012: + func = _draft_checkers["draft202012"].checks(draft202012, raises)( + func, + ) + + # Oy. This is bad global state, but relied upon for now, until + # deprecation. See #519 and test_format_checkers_come_with_defaults + FormatChecker._cls_checks( + draft202012 or draft201909 or draft7 or draft6 or draft4 or draft3, + raises, + )(func) + return func + + return wrap + + +@_checks_drafts(name="idn-email") +@_checks_drafts(name="email") +def is_email(instance: object) -> bool: + if not isinstance(instance, str): + return True + return "@" in instance + + +@_checks_drafts( + draft3="ip-address", + draft4="ipv4", + draft6="ipv4", + draft7="ipv4", + draft201909="ipv4", + draft202012="ipv4", + raises=ipaddress.AddressValueError, +) +def is_ipv4(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(ipaddress.IPv4Address(instance)) + + +@_checks_drafts(name="ipv6", raises=ipaddress.AddressValueError) +def is_ipv6(instance: object) -> bool: + if not isinstance(instance, str): + return True + address = ipaddress.IPv6Address(instance) + return not getattr(address, "scope_id", "") + + +with suppress(ImportError): + from fqdn import FQDN + + @_checks_drafts( + draft3="host-name", + draft4="hostname", + draft6="hostname", + draft7="hostname", + draft201909="hostname", + draft202012="hostname", + ) + def is_host_name(instance: object) -> bool: + if not isinstance(instance, str): + return True + return FQDN(instance).is_valid + + +with suppress(ImportError): + # The built-in `idna` codec only implements RFC 3890, so we go elsewhere. + import idna + + @_checks_drafts( + draft7="idn-hostname", + draft201909="idn-hostname", + draft202012="idn-hostname", + raises=(idna.IDNAError, UnicodeError), + ) + def is_idn_host_name(instance: object) -> bool: + if not isinstance(instance, str): + return True + idna.encode(instance) + return True + + +try: + import rfc3987 +except ImportError: + with suppress(ImportError): + from rfc3986_validator import validate_rfc3986 + + @_checks_drafts(name="uri") + def is_uri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3986(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + draft201909="uri-reference", + draft202012="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3986(instance, rule="URI_reference") + +else: + + @_checks_drafts( + draft7="iri", + draft201909="iri", + draft202012="iri", + raises=ValueError, + ) + def is_iri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="IRI") + + @_checks_drafts( + draft7="iri-reference", + draft201909="iri-reference", + draft202012="iri-reference", + raises=ValueError, + ) + def is_iri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="IRI_reference") + + @_checks_drafts(name="uri", raises=ValueError) + def is_uri(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + draft201909="uri-reference", + draft202012="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance: object) -> bool: + if not isinstance(instance, str): + return True + return rfc3987.parse(instance, rule="URI_reference") + + +with suppress(ImportError): + from rfc3339_validator import validate_rfc3339 + + @_checks_drafts(name="date-time") + def is_datetime(instance: object) -> bool: + if not isinstance(instance, str): + return True + return validate_rfc3339(instance.upper()) + + @_checks_drafts( + draft7="time", + draft201909="time", + draft202012="time", + ) + def is_time(instance: object) -> bool: + if not isinstance(instance, str): + return True + return is_datetime("1970-01-01T" + instance) + + +@_checks_drafts(name="regex", raises=re.error) +def is_regex(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(re.compile(instance)) + + +@_checks_drafts( + draft3="date", + draft7="date", + draft201909="date", + draft202012="date", + raises=ValueError, +) +def is_date(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool( + _RE_DATE.fullmatch(instance) + and datetime.date.fromisoformat(instance) + ) + + +@_checks_drafts(draft3="time", raises=ValueError) +def is_draft3_time(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(datetime.datetime.strptime(instance, "%H:%M:%S")) + + +with suppress(ImportError): + from webcolors import CSS21_NAMES_TO_HEX + import webcolors + + def is_css_color_code(instance: object) -> bool: + return webcolors.normalize_hex(instance) + + @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) + def is_css21_color(instance: object) -> bool: + if ( + not isinstance(instance, str) + or instance.lower() in CSS21_NAMES_TO_HEX + ): + return True + return is_css_color_code(instance) + + +with suppress(ImportError): + import jsonpointer + + @_checks_drafts( + draft6="json-pointer", + draft7="json-pointer", + draft201909="json-pointer", + draft202012="json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_json_pointer(instance: object) -> bool: + if not isinstance(instance, str): + return True + return bool(jsonpointer.JsonPointer(instance)) + + # TODO: I don't want to maintain this, so it + # needs to go either into jsonpointer (pending + # https://github.com/stefankoegl/python-json-pointer/issues/34) or + # into a new external library. + @_checks_drafts( + draft7="relative-json-pointer", + draft201909="relative-json-pointer", + draft202012="relative-json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_relative_json_pointer(instance: object) -> bool: + # Definition taken from: + # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 + if not isinstance(instance, str): + return True + if not instance: + return False + + non_negative_integer, rest = [], "" + for i, character in enumerate(instance): + if character.isdigit(): + # digits with a leading "0" are not allowed + if i > 0 and int(instance[i - 1]) == 0: + return False + + non_negative_integer.append(character) + continue + + if not non_negative_integer: + return False + + rest = instance[i:] + break + return (rest == "#") or bool(jsonpointer.JsonPointer(rest)) + + +with suppress(ImportError): + import uri_template + + @_checks_drafts( + draft6="uri-template", + draft7="uri-template", + draft201909="uri-template", + draft202012="uri-template", + ) + def is_uri_template(instance: object) -> bool: + if not isinstance(instance, str): + return True + return uri_template.validate(instance) + + +with suppress(ImportError): + import isoduration + + @_checks_drafts( + draft201909="duration", + draft202012="duration", + raises=isoduration.DurationParsingException, + ) + def is_duration(instance: object) -> bool: + if not isinstance(instance, str): + return True + isoduration.parse_duration(instance) + # FIXME: See bolsote/isoduration#25 and bolsote/isoduration#21 + return instance.endswith(tuple("DMYWHMS")) + + +@_checks_drafts( + draft201909="uuid", + draft202012="uuid", + raises=ValueError, +) +def is_uuid(instance: object) -> bool: + if not isinstance(instance, str): + return True + UUID(instance) + return all(instance[position] == "-" for position in (8, 13, 18, 23)) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py new file mode 100644 index 0000000..a338624 --- /dev/null +++ b/jsonschema/_legacy_validators.py @@ -0,0 +1,300 @@ +from referencing.jsonschema import lookup_recursive_ref + +from jsonschema import _utils +from jsonschema.exceptions import ValidationError + + +def ignore_ref_siblings(schema): + """ + Ignore siblings of ``$ref`` if it is present. + + Otherwise, return all keywords. + + Suitable for use with `create`'s ``applicable_validators`` argument. + """ + ref = schema.get("$ref") + if ref is not None: + return [("$ref", ref)] + else: + return schema.items() + + +def dependencies_draft3(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependencies.items(): + if property not in instance: + continue + + if validator.is_type(dependency, "object"): + yield from validator.descend( + instance, dependency, schema_path=property, + ) + elif validator.is_type(dependency, "string"): + if dependency not in instance: + message = f"{dependency!r} is a dependency of {property!r}" + yield ValidationError(message) + else: + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + + +def dependencies_draft4_draft6_draft7( + validator, + dependencies, + instance, + schema, +): + """ + Support for the ``dependencies`` keyword from pre-draft 2019-09. + + In later drafts, the keyword was split into separate + ``dependentRequired`` and ``dependentSchemas`` validators. + """ + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependencies.items(): + if property not in instance: + continue + + if validator.is_type(dependency, "array"): + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + else: + yield from validator.descend( + instance, dependency, schema_path=property, + ) + + +def disallow_draft3(validator, disallow, instance, schema): + for disallowed in _utils.ensure_list(disallow): + if validator.evolve(schema={"type": [disallowed]}).is_valid(instance): + message = f"{disallowed!r} is disallowed for {instance!r}" + yield ValidationError(message) + + +def extends_draft3(validator, extends, instance, schema): + if validator.is_type(extends, "object"): + yield from validator.descend(instance, extends) + return + for index, subschema in enumerate(extends): + yield from validator.descend(instance, subschema, schema_path=index) + + +def items_draft3_draft4(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "object"): + for index, item in enumerate(instance): + yield from validator.descend(item, items, path=index) + else: + for (index, item), subschema in zip(enumerate(instance), items): + yield from validator.descend( + item, subschema, path=index, schema_path=index, + ) + + +def items_draft6_draft7_draft201909(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "array"): + for (index, item), subschema in zip(enumerate(instance), items): + yield from validator.descend( + item, subschema, path=index, schema_path=index, + ) + else: + for index, item in enumerate(instance): + yield from validator.descend(item, items, path=index) + + +def minimum_draft3_draft4(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + message = f"{instance!r} is {cmp} the minimum of {minimum!r}" + yield ValidationError(message) + + +def maximum_draft3_draft4(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + message = f"{instance!r} is {cmp} the maximum of {maximum!r}" + yield ValidationError(message) + + +def properties_draft3(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in properties.items(): + if property in instance: + yield from validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ) + elif subschema.get("required", False): + error = ValidationError(f"{property!r} is a required property") + error._set( + validator="required", + validator_value=subschema["required"], + instance=instance, + schema=schema, + ) + error.path.appendleft(property) + error.schema_path.extend([property, "required"]) + yield error + + +def type_draft3(validator, types, instance, schema): + types = _utils.ensure_list(types) + + all_errors = [] + for index, type in enumerate(types): + if validator.is_type(type, "object"): + errors = list(validator.descend(instance, type, schema_path=index)) + if not errors: + return + all_errors.extend(errors) + else: + if validator.is_type(instance, type): + return + else: + reprs = [] + for type in types: + try: + reprs.append(repr(type["name"])) + except Exception: + reprs.append(repr(type)) + yield ValidationError( + f"{instance!r} is not of type {', '.join(reprs)}", + context=all_errors, + ) + + +def contains_draft6_draft7(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + if not any( + validator.evolve(schema=contains).is_valid(element) + for element in instance + ): + yield ValidationError( + f"None of {instance!r} are valid under the given schema", + ) + + +def recursiveRef(validator, recursiveRef, instance, schema): + resolved = lookup_recursive_ref(validator._resolver) + yield from validator.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) + + +def find_evaluated_item_indexes_by_schema(validator, instance, schema): + """ + Get all indexes of items that get evaluated under the current schema. + + Covers all keywords related to unevaluatedItems: items, prefixItems, if, + then, else, contains, unevaluatedItems, allOf, oneOf, anyOf + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_indexes = [] + + if "$ref" in schema: + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "items" in schema: + if "additionalItems" in schema: + return list(range(0, len(instance))) + + if validator.is_type(schema["items"], "object"): + return list(range(0, len(instance))) + evaluated_indexes += list(range(0, len(schema["items"]))) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["then"], + ) + else: + if "else" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["else"], + ) + + for keyword in ["contains", "unevaluatedItems"]: + if keyword in schema: + for k, v in enumerate(instance): + if validator.evolve(schema=schema[keyword]).is_valid(v): + evaluated_indexes.append(k) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, subschema, + ) + + return evaluated_indexes + + +def unevaluatedItems_draft2019(validator, unevaluatedItems, instance, schema): + if not validator.is_type(instance, "array"): + return + evaluated_item_indexes = find_evaluated_item_indexes_by_schema( + validator, instance, schema, + ) + unevaluated_items = [ + item for index, item in enumerate(instance) + if index not in evaluated_item_indexes + ] + if unevaluated_items: + error = "Unevaluated items are not allowed (%s %s unexpected)" + yield ValidationError(error % _utils.extras_msg(unevaluated_items)) diff --git a/jsonschema/_types.py b/jsonschema/_types.py new file mode 100644 index 0000000..d142810 --- /dev/null +++ b/jsonschema/_types.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from typing import Any, Callable, Mapping +import numbers + +from rpds import HashTrieMap +import attr + +from jsonschema.exceptions import UndefinedTypeCheck + + +# unfortunately, the type of HashTrieMap is generic, and if used as the attr.ib +# converter, the generic type is presented to mypy, which then fails to match +# the concrete type of a type checker mapping +# this "do nothing" wrapper presents the correct information to mypy +def _typed_map_converter( + init_val: Mapping[str, Callable[[TypeChecker, Any], bool]], +) -> HashTrieMap[str, Callable[[TypeChecker, Any], bool]]: + return HashTrieMap.convert(init_val) + + +def is_array(checker, instance): + return isinstance(instance, list) + + +def is_bool(checker, instance): + return isinstance(instance, bool) + + +def is_integer(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, int) + + +def is_null(checker, instance): + return instance is None + + +def is_number(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, numbers.Number) + + +def is_object(checker, instance): + return isinstance(instance, dict) + + +def is_string(checker, instance): + return isinstance(instance, str) + + +def is_any(checker, instance): + return True + + +@attr.s(frozen=True, repr=False) +class TypeChecker: + """ + A :kw:`type` property checker. + + A `TypeChecker` performs type checking for a `Validator`, converting + between the defined JSON Schema types and some associated Python types or + objects. + + Modifying the behavior just mentioned by redefining which Python objects + are considered to be of which JSON Schema types can be done using + `TypeChecker.redefine` or `TypeChecker.redefine_many`, and types can be + removed via `TypeChecker.remove`. Each of these return a new `TypeChecker`. + + Arguments: + + type_checkers: + + The initial mapping of types to their checking functions. + """ + + _type_checkers: HashTrieMap[ + str, Callable[[TypeChecker, Any], bool], + ] = attr.ib( + default=HashTrieMap(), + converter=_typed_map_converter, + ) + + def __repr__(self): + types = ", ".join(repr(k) for k in sorted(self._type_checkers)) + return f"<{self.__class__.__name__} types={{{types}}}>" + + def is_type(self, instance, type: str) -> bool: + """ + Check if the instance is of the appropriate type. + + Arguments: + + instance: + + The instance to check + + type: + + The name of the type that is expected. + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if ``type`` is unknown to this object. + """ + try: + fn = self._type_checkers[type] + except KeyError: + raise UndefinedTypeCheck(type) from None + + return fn(self, instance) + + def redefine(self, type: str, fn) -> TypeChecker: + """ + Produce a new checker with the given type redefined. + + Arguments: + + type: + + The name of the type to check. + + fn (collections.abc.Callable): + + A callable taking exactly two parameters - the type + checker calling the function and the instance to check. + The function should return true if instance is of this + type and false otherwise. + """ + return self.redefine_many({type: fn}) + + def redefine_many(self, definitions=()) -> TypeChecker: + """ + Produce a new checker with the given types redefined. + + Arguments: + + definitions (dict): + + A dictionary mapping types to their checking functions. + """ + type_checkers = self._type_checkers.update(definitions) + return attr.evolve(self, type_checkers=type_checkers) + + def remove(self, *types) -> TypeChecker: + """ + Produce a new checker with the given types forgotten. + + Arguments: + + types: + + the names of the types to remove. + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if any given type is unknown to this object + """ + type_checkers = self._type_checkers + for each in types: + try: + type_checkers = type_checkers.remove(each) + except KeyError: + raise UndefinedTypeCheck(each) + return attr.evolve(self, type_checkers=type_checkers) + + +draft3_type_checker = TypeChecker( + { + "any": is_any, + "array": is_array, + "boolean": is_bool, + "integer": is_integer, + "object": is_object, + "null": is_null, + "number": is_number, + "string": is_string, + }, +) +draft4_type_checker = draft3_type_checker.remove("any") +draft6_type_checker = draft4_type_checker.redefine( + "integer", + lambda checker, instance: ( + is_integer(checker, instance) + or isinstance(instance, float) and instance.is_integer() + ), +) +draft7_type_checker = draft6_type_checker +draft201909_type_checker = draft7_type_checker +draft202012_type_checker = draft201909_type_checker diff --git a/jsonschema/_typing.py b/jsonschema/_typing.py new file mode 100644 index 0000000..d283dc4 --- /dev/null +++ b/jsonschema/_typing.py @@ -0,0 +1,28 @@ +""" +Some (initially private) typing helpers for jsonschema's types. +""" +from typing import Any, Callable, Iterable, Protocol, Tuple, Union + +import referencing.jsonschema + +from jsonschema.protocols import Validator + + +class SchemaKeywordValidator(Protocol): + def __call__( + self, + validator: Validator, + value: Any, + instance: Any, + schema: referencing.jsonschema.Schema, + ) -> None: + ... + + +id_of = Callable[[referencing.jsonschema.Schema], Union[str, None]] + + +ApplicableValidators = Callable[ + [referencing.jsonschema.Schema], + Iterable[Tuple[str, Any]], +] diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py new file mode 100644 index 0000000..3f1a440 --- /dev/null +++ b/jsonschema/_utils.py @@ -0,0 +1,332 @@ +from collections.abc import Mapping, MutableMapping, Sequence +from urllib.parse import urlsplit +import itertools +import re + + +class URIDict(MutableMapping): + """ + Dictionary which uses normalized URIs as keys. + """ + + def normalize(self, uri): + return urlsplit(uri).geturl() + + def __init__(self, *args, **kwargs): + self.store = dict() + self.store.update(*args, **kwargs) + + def __getitem__(self, uri): + return self.store[self.normalize(uri)] + + def __setitem__(self, uri, value): + self.store[self.normalize(uri)] = value + + def __delitem__(self, uri): + del self.store[self.normalize(uri)] + + def __iter__(self): + return iter(self.store) + + def __len__(self): + return len(self.store) + + def __repr__(self): + return repr(self.store) + + +class Unset: + """ + An as-of-yet unset attribute or unprovided default parameter. + """ + + def __repr__(self): + return "<unset>" + + +def format_as_index(container, indices): + """ + Construct a single string containing indexing operations for the indices. + + For example for a container ``bar``, [1, 2, "foo"] -> bar[1][2]["foo"] + + Arguments: + + container (str): + + A word to use for the thing being indexed + + indices (sequence): + + The indices to format. + """ + + if not indices: + return container + return f"{container}[{']['.join(repr(index) for index in indices)}]" + + +def find_additional_properties(instance, schema): + """ + Return the set of additional properties for the given ``instance``. + + Weeds out properties that should have been validated by ``properties`` and + / or ``patternProperties``. + + Assumes ``instance`` is dict-like already. + """ + + properties = schema.get("properties", {}) + patterns = "|".join(schema.get("patternProperties", {})) + for property in instance: + if property not in properties: + if patterns and re.search(patterns, property): + continue + yield property + + +def extras_msg(extras): + """ + Create an error message for extra items or properties. + """ + + verb = "was" if len(extras) == 1 else "were" + return ", ".join(repr(extra) for extra in sorted(extras)), verb + + +def ensure_list(thing): + """ + Wrap ``thing`` in a list if it's a single str. + + Otherwise, return it unchanged. + """ + + if isinstance(thing, str): + return [thing] + return thing + + +def _mapping_equal(one, two): + """ + Check if two mappings are equal using the semantics of `equal`. + """ + if len(one) != len(two): + return False + return all( + key in two and equal(value, two[key]) + for key, value in one.items() + ) + + +def _sequence_equal(one, two): + """ + Check if two sequences are equal using the semantics of `equal`. + """ + if len(one) != len(two): + return False + return all(equal(i, j) for i, j in zip(one, two)) + + +def equal(one, two): + """ + Check if two things are equal evading some Python type hierarchy semantics. + + Specifically in JSON Schema, evade `bool` inheriting from `int`, + recursing into sequences to do the same. + """ + if isinstance(one, str) or isinstance(two, str): + return one == two + if isinstance(one, Sequence) and isinstance(two, Sequence): + return _sequence_equal(one, two) + if isinstance(one, Mapping) and isinstance(two, Mapping): + return _mapping_equal(one, two) + return unbool(one) == unbool(two) + + +def unbool(element, true=object(), false=object()): + """ + A hack to make True and 1 and False and 0 unique for ``uniq``. + """ + + if element is True: + return true + elif element is False: + return false + return element + + +def uniq(container): + """ + Check if all of a container's elements are unique. + + Tries to rely on the container being recursively sortable, or otherwise + falls back on (slow) brute force. + """ + try: + sort = sorted(unbool(i) for i in container) + sliced = itertools.islice(sort, 1, None) + + for i, j in zip(sort, sliced): + if equal(i, j): + return False + + except (NotImplementedError, TypeError): + seen = [] + for e in container: + e = unbool(e) + + for i in seen: + if equal(i, e): + return False + + seen.append(e) + return True + + +def find_evaluated_item_indexes_by_schema(validator, instance, schema): + """ + Get all indexes of items that get evaluated under the current schema + + Covers all keywords related to unevaluatedItems: items, prefixItems, if, + then, else, contains, unevaluatedItems, allOf, oneOf, anyOf + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_indexes = [] + + if "items" in schema: + return list(range(0, len(instance))) + + if "$ref" in schema: + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_indexes.extend( + find_evaluated_item_indexes_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + if "prefixItems" in schema: + evaluated_indexes += list(range(0, len(schema["prefixItems"]))) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["then"], + ) + else: + if "else" in schema: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, schema["else"], + ) + + for keyword in ["contains", "unevaluatedItems"]: + if keyword in schema: + for k, v in enumerate(instance): + if validator.evolve(schema=schema[keyword]).is_valid(v): + evaluated_indexes.append(k) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_indexes += find_evaluated_item_indexes_by_schema( + validator, instance, subschema, + ) + + return evaluated_indexes + + +def find_evaluated_property_keys_by_schema(validator, instance, schema): + """ + Get all keys of items that get evaluated under the current schema + + Covers all keywords related to unevaluatedProperties: properties, + additionalProperties, unevaluatedProperties, patternProperties, + dependentSchemas, allOf, oneOf, anyOf, if, then, else + """ + if validator.is_type(schema, "boolean"): + return [] + evaluated_keys = [] + + if "$ref" in schema: + resolved = validator._resolver.lookup(schema["$ref"]) + evaluated_keys.extend( + find_evaluated_property_keys_by_schema( + validator.evolve( + schema=resolved.contents, + _resolver=resolved.resolver, + ), + instance, + resolved.contents, + ), + ) + + for keyword in [ + "properties", "additionalProperties", "unevaluatedProperties", + ]: + if keyword in schema: + if validator.is_type(schema[keyword], "boolean"): + for property, value in instance.items(): + if validator.evolve(schema=schema[keyword]).is_valid( + {property: value}, + ): + evaluated_keys.append(property) + + if validator.is_type(schema[keyword], "object"): + for property, subschema in schema[keyword].items(): + if property in instance and validator.evolve( + schema=subschema, + ).is_valid(instance[property]): + evaluated_keys.append(property) + + if "patternProperties" in schema: + for property, value in instance.items(): + for pattern, _ in schema["patternProperties"].items(): + if re.search(pattern, property) and validator.evolve( + schema=schema["patternProperties"], + ).is_valid({property: value}): + evaluated_keys.append(property) + + if "dependentSchemas" in schema: + for property, subschema in schema["dependentSchemas"].items(): + if property not in instance: + continue + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + for keyword in ["allOf", "oneOf", "anyOf"]: + if keyword in schema: + for subschema in schema[keyword]: + errs = list(validator.descend(instance, subschema)) + if not errs: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, subschema, + ) + + if "if" in schema: + if validator.evolve(schema=schema["if"]).is_valid(instance): + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["if"], + ) + if "then" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["then"], + ) + else: + if "else" in schema: + evaluated_keys += find_evaluated_property_keys_by_schema( + validator, instance, schema["else"], + ) + + return evaluated_keys diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py new file mode 100644 index 0000000..45b53c9 --- /dev/null +++ b/jsonschema/_validators.py @@ -0,0 +1,449 @@ +from fractions import Fraction +import re + +from jsonschema._utils import ( + ensure_list, + equal, + extras_msg, + find_additional_properties, + find_evaluated_item_indexes_by_schema, + find_evaluated_property_keys_by_schema, + unbool, + uniq, +) +from jsonschema.exceptions import FormatError, ValidationError + + +def patternProperties(validator, patternProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for pattern, subschema in patternProperties.items(): + for k, v in instance.items(): + if re.search(pattern, k): + yield from validator.descend( + v, subschema, path=k, schema_path=pattern, + ) + + +def propertyNames(validator, propertyNames, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property in instance: + yield from validator.descend(instance=property, schema=propertyNames) + + +def additionalProperties(validator, aP, instance, schema): + if not validator.is_type(instance, "object"): + return + + extras = set(find_additional_properties(instance, schema)) + + if validator.is_type(aP, "object"): + for extra in extras: + yield from validator.descend(instance[extra], aP, path=extra) + elif not aP and extras: + if "patternProperties" in schema: + verb = "does" if len(extras) == 1 else "do" + joined = ", ".join(repr(each) for each in sorted(extras)) + patterns = ", ".join( + repr(each) for each in sorted(schema["patternProperties"]) + ) + error = f"{joined} {verb} not match any of the regexes: {patterns}" + yield ValidationError(error) + else: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(extras)) + + +def items(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + prefix = len(schema.get("prefixItems", [])) + total = len(instance) + if items is False and total > prefix: + message = f"Expected at most {prefix} items, but found {total}" + yield ValidationError(message) + else: + for index in range(prefix, total): + yield from validator.descend( + instance=instance[index], + schema=items, + path=index, + ) + + +def additionalItems(validator, aI, instance, schema): + if ( + not validator.is_type(instance, "array") + or validator.is_type(schema.get("items", {}), "object") + ): + return + + len_items = len(schema.get("items", [])) + if validator.is_type(aI, "object"): + for index, item in enumerate(instance[len_items:], start=len_items): + yield from validator.descend(item, aI, path=index) + elif not aI and len(instance) > len(schema.get("items", [])): + error = "Additional items are not allowed (%s %s unexpected)" + yield ValidationError( + error % extras_msg(instance[len(schema.get("items", [])):]), + ) + + +def const(validator, const, instance, schema): + if not equal(instance, const): + yield ValidationError(f"{const!r} was expected") + + +def contains(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + matches = 0 + min_contains = schema.get("minContains", 1) + max_contains = schema.get("maxContains", len(instance)) + + for each in instance: + if validator.evolve(schema=contains).is_valid(each): + matches += 1 + if matches > max_contains: + yield ValidationError( + "Too many items match the given schema " + f"(expected at most {max_contains})", + validator="maxContains", + validator_value=max_contains, + ) + return + + if matches < min_contains: + if not matches: + yield ValidationError( + f"{instance!r} does not contain items " + "matching the given schema", + ) + else: + yield ValidationError( + "Too few items match the given schema (expected at least " + f"{min_contains} but only {matches} matched)", + validator="minContains", + validator_value=min_contains, + ) + + +def exclusiveMinimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance <= minimum: + yield ValidationError( + f"{instance!r} is less than or equal to " + f"the minimum of {minimum!r}", + ) + + +def exclusiveMaximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance >= maximum: + yield ValidationError( + f"{instance!r} is greater than or equal " + f"to the maximum of {maximum!r}", + ) + + +def minimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance < minimum: + message = f"{instance!r} is less than the minimum of {minimum!r}" + yield ValidationError(message) + + +def maximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance > maximum: + message = f"{instance!r} is greater than the maximum of {maximum!r}" + yield ValidationError(message) + + +def multipleOf(validator, dB, instance, schema): + if not validator.is_type(instance, "number"): + return + + if isinstance(dB, float): + quotient = instance / dB + try: + failed = int(quotient) != quotient + except OverflowError: + # When `instance` is large and `dB` is less than one, + # quotient can overflow to infinity; and then casting to int + # raises an error. + # + # In this case we fall back to Fraction logic, which is + # exact and cannot overflow. The performance is also + # acceptable: we try the fast all-float option first, and + # we know that fraction(dB) can have at most a few hundred + # digits in each part. The worst-case slowdown is therefore + # for already-slow enormous integers or Decimals. + failed = (Fraction(instance) / Fraction(dB)).denominator != 1 + else: + failed = instance % dB + + if failed: + yield ValidationError(f"{instance!r} is not a multiple of {dB}") + + +def minItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) < mI: + yield ValidationError(f"{instance!r} is too short") + + +def maxItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) > mI: + yield ValidationError(f"{instance!r} is too long") + + +def uniqueItems(validator, uI, instance, schema): + if ( + uI + and validator.is_type(instance, "array") + and not uniq(instance) + ): + yield ValidationError(f"{instance!r} has non-unique elements") + + +def pattern(validator, patrn, instance, schema): + if ( + validator.is_type(instance, "string") + and not re.search(patrn, instance) + ): + yield ValidationError(f"{instance!r} does not match {patrn!r}") + + +def format(validator, format, instance, schema): + if validator.format_checker is not None: + try: + validator.format_checker.check(instance, format) + except FormatError as error: + yield ValidationError(error.message, cause=error.cause) + + +def minLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) < mL: + yield ValidationError(f"{instance!r} is too short") + + +def maxLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) > mL: + yield ValidationError(f"{instance!r} is too long") + + +def dependentRequired(validator, dependentRequired, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentRequired.items(): + if property not in instance: + continue + + for each in dependency: + if each not in instance: + message = f"{each!r} is a dependency of {property!r}" + yield ValidationError(message) + + +def dependentSchemas(validator, dependentSchemas, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in dependentSchemas.items(): + if property not in instance: + continue + yield from validator.descend( + instance, dependency, schema_path=property, + ) + + +def enum(validator, enums, instance, schema): + if instance == 0 or instance == 1: + unbooled = unbool(instance) + if all(unbooled != unbool(each) for each in enums): + yield ValidationError(f"{instance!r} is not one of {enums!r}") + elif instance not in enums: + yield ValidationError(f"{instance!r} is not one of {enums!r}") + + +def ref(validator, ref, instance, schema): + yield from validator._validate_reference(ref=ref, instance=instance) + + +def dynamicRef(validator, dynamicRef, instance, schema): + yield from validator._validate_reference(ref=dynamicRef, instance=instance) + + +def type(validator, types, instance, schema): + types = ensure_list(types) + + if not any(validator.is_type(instance, type) for type in types): + reprs = ", ".join(repr(type) for type in types) + yield ValidationError(f"{instance!r} is not of type {reprs}") + + +def properties(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in properties.items(): + if property in instance: + yield from validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ) + + +def required(validator, required, instance, schema): + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + yield ValidationError(f"{property!r} is a required property") + + +def minProperties(validator, mP, instance, schema): + if validator.is_type(instance, "object") and len(instance) < mP: + yield ValidationError(f"{instance!r} does not have enough properties") + + +def maxProperties(validator, mP, instance, schema): + if not validator.is_type(instance, "object"): + return + if validator.is_type(instance, "object") and len(instance) > mP: + yield ValidationError(f"{instance!r} has too many properties") + + +def allOf(validator, allOf, instance, schema): + for index, subschema in enumerate(allOf): + yield from validator.descend(instance, subschema, schema_path=index) + + +def anyOf(validator, anyOf, instance, schema): + all_errors = [] + for index, subschema in enumerate(anyOf): + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + break + all_errors.extend(errs) + else: + yield ValidationError( + f"{instance!r} is not valid under any of the given schemas", + context=all_errors, + ) + + +def oneOf(validator, oneOf, instance, schema): + subschemas = enumerate(oneOf) + all_errors = [] + for index, subschema in subschemas: + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + first_valid = subschema + break + all_errors.extend(errs) + else: + yield ValidationError( + f"{instance!r} is not valid under any of the given schemas", + context=all_errors, + ) + + more_valid = [ + each for _, each in subschemas + if validator.evolve(schema=each).is_valid(instance) + ] + if more_valid: + more_valid.append(first_valid) + reprs = ", ".join(repr(schema) for schema in more_valid) + yield ValidationError(f"{instance!r} is valid under each of {reprs}") + + +def not_(validator, not_schema, instance, schema): + if validator.evolve(schema=not_schema).is_valid(instance): + message = f"{instance!r} should not be valid under {not_schema!r}" + yield ValidationError(message) + + +def if_(validator, if_schema, instance, schema): + if validator.evolve(schema=if_schema).is_valid(instance): + if "then" in schema: + then = schema["then"] + yield from validator.descend(instance, then, schema_path="then") + elif "else" in schema: + else_ = schema["else"] + yield from validator.descend(instance, else_, schema_path="else") + + +def unevaluatedItems(validator, unevaluatedItems, instance, schema): + if not validator.is_type(instance, "array"): + return + evaluated_item_indexes = find_evaluated_item_indexes_by_schema( + validator, instance, schema, + ) + unevaluated_items = [ + item for index, item in enumerate(instance) + if index not in evaluated_item_indexes + ] + if unevaluated_items: + error = "Unevaluated items are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(unevaluated_items)) + + +def unevaluatedProperties(validator, unevaluatedProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + evaluated_keys = find_evaluated_property_keys_by_schema( + validator, instance, schema, + ) + unevaluated_keys = [] + for property in instance: + if property not in evaluated_keys: + for _ in validator.descend( + instance[property], + unevaluatedProperties, + path=property, + schema_path=property, + ): + # FIXME: Include context for each unevaluated property + # indicating why it's invalid under the subschema. + unevaluated_keys.append(property) + + if unevaluated_keys: + if unevaluatedProperties is False: + error = "Unevaluated properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(unevaluated_keys)) + else: + error = ( + "Unevaluated properties are not valid under " + "the given schema (%s %s unevaluated and invalid)" + ) + yield ValidationError(error % extras_msg(unevaluated_keys)) + + +def prefixItems(validator, prefixItems, instance, schema): + if not validator.is_type(instance, "array"): + return + + for (index, item), subschema in zip(enumerate(instance), prefixItems): + yield from validator.descend( + instance=item, + schema=subschema, + schema_path=index, + path=index, + ) diff --git a/jsonschema/benchmarks/__init__.py b/jsonschema/benchmarks/__init__.py new file mode 100644 index 0000000..e3dcc68 --- /dev/null +++ b/jsonschema/benchmarks/__init__.py @@ -0,0 +1,5 @@ +""" +Benchmarks for validation. + +This package is *not* public API. +""" diff --git a/jsonschema/benchmarks/issue232.py b/jsonschema/benchmarks/issue232.py new file mode 100644 index 0000000..efd0715 --- /dev/null +++ b/jsonschema/benchmarks/issue232.py @@ -0,0 +1,25 @@ +""" +A performance benchmark using the example from issue #232. + +See https://github.com/python-jsonschema/jsonschema/pull/232. +""" +from pathlib import Path + +from pyperf import Runner +from referencing import Registry + +from jsonschema.tests._suite import Version +import jsonschema + +issue232 = Version( + path=Path(__file__).parent / "issue232", + remotes=Registry(), + name="issue232", +) + + +if __name__ == "__main__": + issue232.benchmark( + runner=Runner(), + Validator=jsonschema.Draft4Validator, + ) diff --git a/jsonschema/benchmarks/issue232/issue.json b/jsonschema/benchmarks/issue232/issue.json new file mode 100644 index 0000000..804c340 --- /dev/null +++ b/jsonschema/benchmarks/issue232/issue.json @@ -0,0 +1,2653 @@ +[ + { + "description": "Petstore", + "schema": { + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for parameters" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } + }, + "tests": [ + { + "description": "Example petsore", + "data": { + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + "valid": true + } + ] + } +] diff --git a/jsonschema/benchmarks/json_schema_test_suite.py b/jsonschema/benchmarks/json_schema_test_suite.py new file mode 100644 index 0000000..905fb6a --- /dev/null +++ b/jsonschema/benchmarks/json_schema_test_suite.py @@ -0,0 +1,12 @@ +""" +A performance benchmark using the official test suite. + +This benchmarks jsonschema using every valid example in the +JSON-Schema-Test-Suite. It will take some time to complete. +""" +from pyperf import Runner + +from jsonschema.tests._suite import Suite + +if __name__ == "__main__": + Suite().benchmark(runner=Runner()) diff --git a/jsonschema/benchmarks/validator_creation.py b/jsonschema/benchmarks/validator_creation.py new file mode 100644 index 0000000..4baeb3a --- /dev/null +++ b/jsonschema/benchmarks/validator_creation.py @@ -0,0 +1,14 @@ +from pyperf import Runner + +from jsonschema import Draft202012Validator + +schema = { + "type": "array", + "minLength": 1, + "maxLength": 1, + "items": {"type": "integer"}, +} + + +if __name__ == "__main__": + Runner().bench_func("validator creation", Draft202012Validator, schema) diff --git a/jsonschema/cli.py b/jsonschema/cli.py new file mode 100644 index 0000000..d6b9ad9 --- /dev/null +++ b/jsonschema/cli.py @@ -0,0 +1,300 @@ +""" +The ``jsonschema`` command line. +""" + +from json import JSONDecodeError +from textwrap import dedent +import argparse +import json +import sys +import traceback +import warnings + +try: + from importlib import metadata +except ImportError: + import importlib_metadata as metadata # type: ignore + +try: + from pkgutil import resolve_name +except ImportError: + from pkgutil_resolve_name import resolve_name # type: ignore + +import attr + +from jsonschema.exceptions import SchemaError +from jsonschema.validators import _RefResolver, validator_for + +warnings.warn( + ( + "The jsonschema CLI is deprecated and will be removed in a future " + "version. Please use check-jsonschema instead, which can be installed " + "from https://pypi.org/project/check-jsonschema/" + ), + DeprecationWarning, + stacklevel=2, +) + + +class _CannotLoadFile(Exception): + pass + + +@attr.s +class _Outputter: + + _formatter = attr.ib() + _stdout = attr.ib() + _stderr = attr.ib() + + @classmethod + def from_arguments(cls, arguments, stdout, stderr): + if arguments["output"] == "plain": + formatter = _PlainFormatter(arguments["error_format"]) + elif arguments["output"] == "pretty": + formatter = _PrettyFormatter() + return cls(formatter=formatter, stdout=stdout, stderr=stderr) + + def load(self, path): + try: + file = open(path) + except FileNotFoundError: + self.filenotfound_error(path=path, exc_info=sys.exc_info()) + raise _CannotLoadFile() + + with file: + try: + return json.load(file) + except JSONDecodeError: + self.parsing_error(path=path, exc_info=sys.exc_info()) + raise _CannotLoadFile() + + def filenotfound_error(self, **kwargs): + self._stderr.write(self._formatter.filenotfound_error(**kwargs)) + + def parsing_error(self, **kwargs): + self._stderr.write(self._formatter.parsing_error(**kwargs)) + + def validation_error(self, **kwargs): + self._stderr.write(self._formatter.validation_error(**kwargs)) + + def validation_success(self, **kwargs): + self._stdout.write(self._formatter.validation_success(**kwargs)) + + +@attr.s +class _PrettyFormatter: + + _ERROR_MSG = dedent( + """\ + ===[{type}]===({path})=== + + {body} + ----------------------------- + """, + ) + _SUCCESS_MSG = "===[SUCCESS]===({path})===\n" + + def filenotfound_error(self, path, exc_info): + return self._ERROR_MSG.format( + path=path, + type="FileNotFoundError", + body="{!r} does not exist.".format(path), + ) + + def parsing_error(self, path, exc_info): + exc_type, exc_value, exc_traceback = exc_info + exc_lines = "".join( + traceback.format_exception(exc_type, exc_value, exc_traceback), + ) + return self._ERROR_MSG.format( + path=path, + type=exc_type.__name__, + body=exc_lines, + ) + + def validation_error(self, instance_path, error): + return self._ERROR_MSG.format( + path=instance_path, + type=error.__class__.__name__, + body=error, + ) + + def validation_success(self, instance_path): + return self._SUCCESS_MSG.format(path=instance_path) + + +@attr.s +class _PlainFormatter: + + _error_format = attr.ib() + + def filenotfound_error(self, path, exc_info): + return "{!r} does not exist.\n".format(path) + + def parsing_error(self, path, exc_info): + return "Failed to parse {}: {}\n".format( + "<stdin>" if path == "<stdin>" else repr(path), + exc_info[1], + ) + + def validation_error(self, instance_path, error): + return self._error_format.format(file_name=instance_path, error=error) + + def validation_success(self, instance_path): + return "" + + +def _resolve_name_with_default(name): + if "." not in name: + name = "jsonschema." + name + return resolve_name(name) + + +parser = argparse.ArgumentParser( + description="JSON Schema Validation CLI", +) +parser.add_argument( + "-i", "--instance", + action="append", + dest="instances", + help=""" + a path to a JSON instance (i.e. filename.json) to validate (may + be specified multiple times). If no instances are provided via this + option, one will be expected on standard input. + """, +) +parser.add_argument( + "-F", "--error-format", + help=""" + the format to use for each validation error message, specified + in a form suitable for str.format. This string will be passed + one formatted object named 'error' for each ValidationError. + Only provide this option when using --output=plain, which is the + default. If this argument is unprovided and --output=plain is + used, a simple default representation will be used. + """, +) +parser.add_argument( + "-o", "--output", + choices=["plain", "pretty"], + default="plain", + help=""" + an output format to use. 'plain' (default) will produce minimal + text with one line for each error, while 'pretty' will produce + more detailed human-readable output on multiple lines. + """, +) +parser.add_argument( + "-V", "--validator", + type=_resolve_name_with_default, + help=""" + the fully qualified object name of a validator to use, or, for + validators that are registered with jsonschema, simply the name + of the class. + """, +) +parser.add_argument( + "--base-uri", + help=""" + a base URI to assign to the provided schema, even if it does not + declare one (via e.g. $id). This option can be used if you wish to + resolve relative references to a particular URI (or local path) + """, +) +parser.add_argument( + "--version", + action="version", + version=metadata.version("jsonschema"), +) +parser.add_argument( + "schema", + help="the path to a JSON Schema to validate with (i.e. schema.json)", +) + + +def parse_args(args): + arguments = vars(parser.parse_args(args=args or ["--help"])) + if arguments["output"] != "plain" and arguments["error_format"]: + raise parser.error( + "--error-format can only be used with --output plain", + ) + if arguments["output"] == "plain" and arguments["error_format"] is None: + arguments["error_format"] = "{error.instance}: {error.message}\n" + return arguments + + +def _validate_instance(instance_path, instance, validator, outputter): + invalid = False + for error in validator.iter_errors(instance): + invalid = True + outputter.validation_error(instance_path=instance_path, error=error) + + if not invalid: + outputter.validation_success(instance_path=instance_path) + return invalid + + +def main(args=sys.argv[1:]): + sys.exit(run(arguments=parse_args(args=args))) + + +def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): + outputter = _Outputter.from_arguments( + arguments=arguments, + stdout=stdout, + stderr=stderr, + ) + + try: + schema = outputter.load(arguments["schema"]) + except _CannotLoadFile: + return 1 + + Validator = arguments["validator"] + if Validator is None: + Validator = validator_for(schema) + + try: + Validator.check_schema(schema) + except SchemaError as error: + outputter.validation_error( + instance_path=arguments["schema"], + error=error, + ) + return 1 + + if arguments["instances"]: + load, instances = outputter.load, arguments["instances"] + else: + def load(_): + try: + return json.load(stdin) + except JSONDecodeError: + outputter.parsing_error( + path="<stdin>", exc_info=sys.exc_info(), + ) + raise _CannotLoadFile() + instances = ["<stdin>"] + + resolver = _RefResolver( + base_uri=arguments["base_uri"], + referrer=schema, + ) if arguments["base_uri"] is not None else None + + validator = Validator(schema, resolver=resolver) + exit_code = 0 + for each in instances: + try: + instance = load(each) + except _CannotLoadFile: + exit_code = 1 + else: + exit_code |= _validate_instance( + instance_path=each, + instance=instance, + validator=validator, + outputter=outputter, + ) + + return exit_code diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py new file mode 100644 index 0000000..d1ee50a --- /dev/null +++ b/jsonschema/exceptions.py @@ -0,0 +1,426 @@ +""" +Validation errors, and some surrounding helpers. +""" +from __future__ import annotations + +from collections import defaultdict, deque +from pprint import pformat +from textwrap import dedent, indent +from typing import ClassVar +import heapq +import itertools +import warnings + +import attr + +from jsonschema import _utils + +WEAK_MATCHES: frozenset[str] = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES: frozenset[str] = frozenset() + +_unset = _utils.Unset() + + +def __getattr__(name): + if name == "RefResolutionError": + warnings.warn( + _RefResolutionError._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolutionError + raise AttributeError(f"module {__name__} has no attribute {name}") + + +class _Error(Exception): + + _word_for_schema_in_error_message: ClassVar[str] + _word_for_instance_in_error_message: ClassVar[str] + + def __init__( + self, + message: str, + validator=_unset, + path=(), + cause=None, + context=(), + validator_value=_unset, + instance=_unset, + schema=_unset, + schema_path=(), + parent=None, + type_checker=_unset, + ): + super().__init__( + message, + validator, + path, + cause, + context, + validator_value, + instance, + schema, + schema_path, + parent, + ) + self.message = message + self.path = self.relative_path = deque(path) + self.schema_path = self.relative_schema_path = deque(schema_path) + self.context = list(context) + self.cause = self.__cause__ = cause + self.validator = validator + self.validator_value = validator_value + self.instance = instance + self.schema = schema + self.parent = parent + self._type_checker = type_checker + + for error in context: + error.parent = self + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.message!r}>" + + def __str__(self): + essential_for_verbose = ( + self.validator, self.validator_value, self.instance, self.schema, + ) + if any(m is _unset for m in essential_for_verbose): + return self.message + + schema_path = _utils.format_as_index( + container=self._word_for_schema_in_error_message, + indices=list(self.relative_schema_path)[:-1], + ) + instance_path = _utils.format_as_index( + container=self._word_for_instance_in_error_message, + indices=self.relative_path, + ) + prefix = 16 * " " + + return dedent( + f"""\ + {self.message} + + Failed validating {self.validator!r} in {schema_path}: + {indent(pformat(self.schema, width=72), prefix).lstrip()} + + On {instance_path}: + {indent(pformat(self.instance, width=72), prefix).lstrip()} + """.rstrip(), + ) + + @classmethod + def create_from(cls, other): + return cls(**other._contents()) + + @property + def absolute_path(self): + parent = self.parent + if parent is None: + return self.relative_path + + path = deque(self.relative_path) + path.extendleft(reversed(parent.absolute_path)) + return path + + @property + def absolute_schema_path(self): + parent = self.parent + if parent is None: + return self.relative_schema_path + + path = deque(self.relative_schema_path) + path.extendleft(reversed(parent.absolute_schema_path)) + return path + + @property + def json_path(self): + path = "$" + for elem in self.absolute_path: + if isinstance(elem, int): + path += "[" + str(elem) + "]" + else: + path += "." + elem + return path + + def _set(self, type_checker=None, **kwargs): + if type_checker is not None and self._type_checker is _unset: + self._type_checker = type_checker + + for k, v in kwargs.items(): + if getattr(self, k) is _unset: + setattr(self, k, v) + + def _contents(self): + attrs = ( + "message", "cause", "context", "validator", "validator_value", + "path", "schema_path", "instance", "schema", "parent", + ) + return dict((attr, getattr(self, attr)) for attr in attrs) + + def _matches_type(self): + try: + expected = self.schema["type"] + except (KeyError, TypeError): + return False + + if isinstance(expected, str): + return self._type_checker.is_type(self.instance, expected) + + return any( + self._type_checker.is_type(self.instance, expected_type) + for expected_type in expected + ) + + +class ValidationError(_Error): + """ + An instance was invalid under a provided schema. + """ + + _word_for_schema_in_error_message = "schema" + _word_for_instance_in_error_message = "instance" + + +class SchemaError(_Error): + """ + A schema was invalid under its corresponding metaschema. + """ + + _word_for_schema_in_error_message = "metaschema" + _word_for_instance_in_error_message = "schema" + + +@attr.s(hash=True) +class _RefResolutionError(Exception): + """ + A ref could not be resolved. + """ + + _DEPRECATION_MESSAGE = ( + "jsonschema.exceptions.RefResolutionError is deprecated as of version " + "4.18.0. If you wish to catch potential reference resolution errors, " + "directly catch referencing.exceptions.Unresolvable." + ) + + _cause = attr.ib() + + def __str__(self): + return str(self._cause) + + +class UndefinedTypeCheck(Exception): + """ + A type checker was asked to check a type it did not have registered. + """ + + def __init__(self, type): + self.type = type + + def __str__(self): + return f"Type {self.type!r} is unknown to this type checker" + + +class UnknownType(Exception): + """ + A validator was asked to validate an instance against an unknown type. + """ + + def __init__(self, type, instance, schema): + self.type = type + self.instance = instance + self.schema = schema + + def __str__(self): + prefix = 16 * " " + + return dedent( + f"""\ + Unknown type {self.type!r} for validator with schema: + {indent(pformat(self.schema, width=72), prefix).lstrip()} + + While checking instance: + {indent(pformat(self.instance, width=72), prefix).lstrip()} + """.rstrip(), + ) + + +class FormatError(Exception): + """ + Validating a format failed. + """ + + def __init__(self, message, cause=None): + super().__init__(message, cause) + self.message = message + self.cause = self.__cause__ = cause + + def __str__(self): + return self.message + + +class ErrorTree: + """ + ErrorTrees make it easier to check which validations failed. + """ + + _instance = _unset + + def __init__(self, errors=()): + self.errors = {} + self._contents = defaultdict(self.__class__) + + for error in errors: + container = self + for element in error.path: + container = container[element] + container.errors[error.validator] = error + + container._instance = error.instance + + def __contains__(self, index): + """ + Check whether ``instance[index]`` has any errors. + """ + return index in self._contents + + def __getitem__(self, index): + """ + Retrieve the child tree one level down at the given ``index``. + + If the index is not in the instance that this tree corresponds + to and is not known by this tree, whatever error would be raised + by ``instance.__getitem__`` will be propagated (usually this is + some subclass of `LookupError`. + """ + if self._instance is not _unset and index not in self: + self._instance[index] + return self._contents[index] + + def __setitem__(self, index, value): + """ + Add an error to the tree at the given ``index``. + """ + self._contents[index] = value + + def __iter__(self): + """ + Iterate (non-recursively) over the indices in the instance with errors. + """ + return iter(self._contents) + + def __len__(self): + """ + Return the `total_errors`. + """ + return self.total_errors + + def __repr__(self): + total = len(self) + errors = "error" if total == 1 else "errors" + return f"<{self.__class__.__name__} ({total} total {errors})>" + + @property + def total_errors(self): + """ + The total number of errors in the entire tree, including children. + """ + child_errors = sum(len(tree) for _, tree in self._contents.items()) + return len(self.errors) + child_errors + + +def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): + """ + Create a key function that can be used to sort errors by relevance. + + Arguments: + weak (set): + a collection of validation keywords to consider to be + "weak". If there are two errors at the same level of the + instance and one is in the set of weak validation keywords, + the other error will take priority. By default, :kw:`anyOf` + and :kw:`oneOf` are considered weak keywords and will be + superseded by other same-level validation errors. + + strong (set): + a collection of validation keywords to consider to be + "strong" + """ + + def relevance(error): + validator = error.validator + return ( + -len(error.path), + validator not in weak, + validator in strong, + not error._matches_type(), + ) + + return relevance + + +relevance = by_relevance() +""" +A key function (e.g. to use with `sorted`) which sorts errors by relevance. + +Example: + +.. code:: python + + sorted(validator.iter_errors(12), key=jsonschema.exceptions.relevance) +""" + + +def best_match(errors, key=relevance): + """ + Try to find an error that appears to be the best match among given errors. + + In general, errors that are higher up in the instance (i.e. for which + `ValidationError.path` is shorter) are considered better matches, + since they indicate "more" is wrong with the instance. + + If the resulting match is either :kw:`oneOf` or :kw:`anyOf`, the + *opposite* assumption is made -- i.e. the deepest error is picked, + since these keywords only need to match once, and any other errors + may not be relevant. + + Arguments: + errors (collections.abc.Iterable): + + the errors to select from. Do not provide a mixture of + errors from different validation attempts (i.e. from + different instances or schemas), since it won't produce + sensical output. + + key (collections.abc.Callable): + + the key to use when sorting errors. See `relevance` and + transitively `by_relevance` for more details (the default is + to sort with the defaults of that function). Changing the + default is only useful if you want to change the function + that rates errors but still want the error context descent + done by this function. + + Returns: + the best matching error, or ``None`` if the iterable was empty + + .. note:: + + This function is a heuristic. Its return value may change for a given + set of inputs from version to version if better heuristics are added. + """ + errors = iter(errors) + best = next(errors, None) + if best is None: + return + best = max(itertools.chain([best], errors), key=key) + + while best.context: + # Calculate the minimum via nsmallest, because we don't recurse if + # all nested errors have the same relevance (i.e. if min == max == all) + smallest = heapq.nsmallest(2, best.context, key=key) + if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]): + return best + best = smallest[0] + return best diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py new file mode 100644 index 0000000..4ad43e7 --- /dev/null +++ b/jsonschema/protocols.py @@ -0,0 +1,230 @@ +""" +typing.Protocol classes for jsonschema interfaces. +""" + +# for reference material on Protocols, see +# https://www.python.org/dev/peps/pep-0544/ + +from __future__ import annotations + +from collections.abc import Mapping +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Iterable, + Protocol, + runtime_checkable, +) + +# in order for Sphinx to resolve references accurately from type annotations, +# it needs to see names like `jsonschema.TypeChecker` +# therefore, only import at type-checking time (to avoid circular references), +# but use `jsonschema` for any types which will otherwise not be resolvable +if TYPE_CHECKING: + from jsonschema import _typing + import jsonschema + import jsonschema.validators + import referencing.jsonschema + +from jsonschema.exceptions import ValidationError + +# For code authors working on the validator protocol, these are the three +# use-cases which should be kept in mind: +# +# 1. As a protocol class, it can be used in type annotations to describe the +# available methods and attributes of a validator +# 2. It is the source of autodoc for the validator documentation +# 3. It is runtime_checkable, meaning that it can be used in isinstance() +# checks. +# +# Since protocols are not base classes, isinstance() checking is limited in +# its capabilities. See docs on runtime_checkable for detail + + +@runtime_checkable +class Validator(Protocol): + """ + The protocol to which all validator classes adhere. + + Arguments: + + schema: + + The schema that the validator object will validate with. + It is assumed to be valid, and providing + an invalid schema can lead to undefined behavior. See + `Validator.check_schema` to validate a schema first. + + registry: + + a schema registry that will be used for looking up JSON references + + resolver: + + a resolver that will be used to resolve :kw:`$ref` + properties (JSON references). If unprovided, one will be created. + + .. deprecated:: v4.18.0 + + `RefResolver <_RefResolver>` has been deprecated in favor of + `referencing`, and with it, this argument. + + format_checker: + + if provided, a checker which will be used to assert about + :kw:`format` properties present in the schema. If unprovided, + *no* format validation is done, and the presence of format + within schemas is strictly informational. Certain formats + require additional packages to be installed in order to assert + against instances. Ensure you've installed `jsonschema` with + its `extra (optional) dependencies <index:extras>` when + invoking ``pip``. + + .. deprecated:: v4.12.0 + + Subclassing validator classes now explicitly warns this is not part of + their public API. + """ + + #: An object representing the validator's meta schema (the schema that + #: describes valid schemas in the given version). + META_SCHEMA: ClassVar[Mapping] + + #: A mapping of validation keywords (`str`\s) to functions that + #: validate the keyword with that name. For more information see + #: `creating-validators`. + VALIDATORS: ClassVar[Mapping] + + #: A `jsonschema.TypeChecker` that will be used when validating + #: :kw:`type` keywords in JSON schemas. + TYPE_CHECKER: ClassVar[jsonschema.TypeChecker] + + #: A `jsonschema.FormatChecker` that will be used when validating + #: :kw:`format` keywords in JSON schemas. + FORMAT_CHECKER: ClassVar[jsonschema.FormatChecker] + + #: A function which given a schema returns its ID. + ID_OF: _typing.id_of + + #: The schema that will be used to validate instances + schema: Mapping | bool + + def __init__( + self, + schema: Mapping | bool, + registry: referencing.jsonschema.SchemaRegistry, + format_checker: jsonschema.FormatChecker | None = None, + ) -> None: + ... + + @classmethod + def check_schema(cls, schema: Mapping | bool) -> None: + """ + Validate the given schema against the validator's `META_SCHEMA`. + + Raises: + + `jsonschema.exceptions.SchemaError`: + + if the schema is invalid + """ + + def is_type(self, instance: Any, type: str) -> bool: + """ + Check if the instance is of the given (JSON Schema) type. + + Arguments: + + instance: + + the value to check + + type: + + the name of a known (JSON Schema) type + + Returns: + + whether the instance is of the given type + + Raises: + + `jsonschema.exceptions.UnknownType`: + + if ``type`` is not a known type + """ + + def is_valid(self, instance: Any) -> bool: + """ + Check if the instance is valid under the current `schema`. + + Returns: + + whether the instance is valid or not + + >>> schema = {"maxItems" : 2} + >>> Draft202012Validator(schema).is_valid([2, 3, 4]) + False + """ + + def iter_errors(self, instance: Any) -> Iterable[ValidationError]: + r""" + Lazily yield each of the validation errors in the given instance. + + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Draft202012Validator(schema) + >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): + ... print(error.message) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + + .. deprecated:: v4.0.0 + + Calling this function with a second schema argument is deprecated. + Use `Validator.evolve` instead. + """ + + def validate(self, instance: Any) -> None: + """ + Check if the instance is valid under the current `schema`. + + Raises: + + `jsonschema.exceptions.ValidationError`: + + if the instance is invalid + + >>> schema = {"maxItems" : 2} + >>> Draft202012Validator(schema).validate([2, 3, 4]) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + """ + + def evolve(self, **kwargs) -> Validator: + """ + Create a new validator like this one, but with given changes. + + Preserves all other attributes, so can be used to e.g. create a + validator with a different schema but with the same :kw:`$ref` + resolution behavior. + + >>> validator = Draft202012Validator({}) + >>> validator.evolve(schema={"type": "number"}) + Draft202012Validator(schema={'type': 'number'}, format_checker=None) + + The returned object satisfies the validator protocol, but may not + be of the same concrete class! In particular this occurs + when a :kw:`$ref` occurs to a schema with a different + :kw:`$schema` than this one (i.e. for a different draft). + + >>> validator.evolve( + ... schema={"$schema": Draft7Validator.META_SCHEMA["$id"]} + ... ) + Draft7Validator(schema=..., format_checker=None) + """ diff --git a/jsonschema/tests/__init__.py b/jsonschema/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/jsonschema/tests/__init__.py diff --git a/jsonschema/tests/_helpers.py b/jsonschema/tests/_helpers.py new file mode 100644 index 0000000..754ff83 --- /dev/null +++ b/jsonschema/tests/_helpers.py @@ -0,0 +1,25 @@ +from urllib.parse import urljoin + + +def issues_url(organization, repository): + return urljoin( + "https://github.com/", f"{organization}/{repository}/issues/", + ) + + +ISSUES_URL = issues_url("python-jsonschema", "jsonschema") +TEST_SUITE_ISSUES_URL = issues_url("json-schema-org", "JSON-Schema-Test-Suite") + + +def bug(issue=None): + message = "A known bug." + if issue is not None: + message += f" See {urljoin(ISSUES_URL, str(issue))}." + return message + + +def test_suite_bug(issue): + return ( + "A known test suite bug. " + f"See {urljoin(TEST_SUITE_ISSUES_URL, str(issue))}." + ) diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py new file mode 100644 index 0000000..a79b1e2 --- /dev/null +++ b/jsonschema/tests/_suite.py @@ -0,0 +1,274 @@ +""" +Python representations of the JSON Schema Test Suite tests. +""" +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from contextlib import suppress +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Any +import json +import os +import re +import subprocess +import sys +import unittest + +from attrs import field, frozen +from referencing import Registry +import referencing.jsonschema + +if TYPE_CHECKING: + import pyperf + +from jsonschema.validators import _VALIDATORS +import jsonschema + +_DELIMITERS = re.compile(r"[\W\- ]+") + + +def _find_suite(): + root = os.environ.get("JSON_SCHEMA_TEST_SUITE") + if root is not None: + return Path(root) + + root = Path(jsonschema.__file__).parent.parent / "json" + if not root.is_dir(): # pragma: no cover + raise ValueError( + ( + "Can't find the JSON-Schema-Test-Suite directory. " + "Set the 'JSON_SCHEMA_TEST_SUITE' environment " + "variable or run the tests from alongside a checkout " + "of the suite." + ), + ) + return root + + +@frozen +class Suite: + + _root: Path = field(factory=_find_suite) + _remotes: referencing.jsonschema.SchemaRegistry = field(init=False) + + def __attrs_post_init__(self): + jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite") + argv = [sys.executable, str(jsonschema_suite), "remotes"] + remotes = subprocess.check_output(argv).decode("utf-8") + + resources = json.loads(remotes) + + li = "http://localhost:1234/locationIndependentIdentifierPre2019.json" + li4 = "http://localhost:1234/locationIndependentIdentifierDraft4.json" + + registry = Registry().with_resources( + [ + ( + li, + referencing.jsonschema.DRAFT7.create_resource( + contents=resources.pop(li), + ), + ), + ( + li4, + referencing.jsonschema.DRAFT4.create_resource( + contents=resources.pop(li4), + ), + ), + ], + ).with_contents( + resources.items(), + default_specification=referencing.jsonschema.DRAFT202012, + ) + object.__setattr__(self, "_remotes", registry) + + def benchmark(self, runner: pyperf.Runner): # pragma: no cover + for name, Validator in _VALIDATORS.items(): + self.version(name=name).benchmark( + runner=runner, + Validator=Validator, + ) + + def version(self, name) -> Version: + return Version( + name=name, + path=self._root / "tests" / name, + remotes=self._remotes, + ) + + +@frozen +class Version: + + _path: Path + _remotes: referencing.jsonschema.SchemaRegistry + + name: str + + def benchmark(self, **kwargs): # pragma: no cover + for case in self.cases(): + case.benchmark(**kwargs) + + def cases(self) -> Iterable[_Case]: + return self._cases_in(paths=self._path.glob("*.json")) + + def format_cases(self) -> Iterable[_Case]: + return self._cases_in(paths=self._path.glob("optional/format/*.json")) + + def optional_cases_of(self, name: str) -> Iterable[_Case]: + return self._cases_in(paths=[self._path / "optional" / f"{name}.json"]) + + def to_unittest_testcase(self, *groups, **kwargs): + name = kwargs.pop("name", "Test" + self.name.title().replace("-", "")) + methods = { + method.__name__: method + for method in ( + test.to_unittest_method(**kwargs) + for group in groups + for case in group + for test in case.tests + ) + } + cls = type(name, (unittest.TestCase,), methods) + + # We're doing crazy things, so if they go wrong, like a function + # behaving differently on some other interpreter, just make them + # not happen. + with suppress(Exception): + cls.__module__ = _someone_save_us_the_module_of_the_caller() + + return cls + + def _cases_in(self, paths: Iterable[Path]) -> Iterable[_Case]: + for path in paths: + for case in json.loads(path.read_text(encoding="utf-8")): + yield _Case.from_dict( + case, + version=self, + subject=path.stem, + remotes=self._remotes, + ) + + +@frozen +class _Case: + + version: Version + + subject: str + description: str + schema: Mapping[str, Any] | bool + tests: list[_Test] + comment: str | None = None + + @classmethod + def from_dict(cls, data, remotes, **kwargs): + data.update(kwargs) + tests = [ + _Test( + version=data["version"], + subject=data["subject"], + case_description=data["description"], + schema=data["schema"], + remotes=remotes, + **test, + ) for test in data.pop("tests") + ] + return cls(tests=tests, **data) + + def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover + for test in self.tests: + runner.bench_func( + test.fully_qualified_name, + partial(test.validate_ignoring_errors, **kwargs), + ) + + +@frozen(repr=False) +class _Test: + + version: Version + + subject: str + case_description: str + description: str + + data: Any + schema: Mapping[str, Any] | bool + + valid: bool + + _remotes: referencing.jsonschema.SchemaRegistry + + comment: str | None = None + + def __repr__(self): # pragma: no cover + return f"<Test {self.fully_qualified_name}>" + + @property + def fully_qualified_name(self): # pragma: no cover + return " > ".join( + [ + self.version.name, + self.subject, + self.case_description, + self.description, + ], + ) + + def to_unittest_method(self, skip=lambda test: None, **kwargs): + if self.valid: + def fn(this): + self.validate(**kwargs) + else: + def fn(this): + with this.assertRaises(jsonschema.ValidationError): + self.validate(**kwargs) + + fn.__name__ = "_".join( + [ + "test", + _DELIMITERS.sub("_", self.subject), + _DELIMITERS.sub("_", self.case_description), + _DELIMITERS.sub("_", self.description), + ], + ) + reason = skip(self) + if reason is None or os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": + return fn + elif os.environ.get("JSON_SCHEMA_EXPECTED_FAILURES", "0") != "0": + return unittest.expectedFailure(fn) + else: + return unittest.skip(reason)(fn) + + def validate(self, Validator, **kwargs): + Validator.check_schema(self.schema) + validator = Validator( + schema=self.schema, + registry=self._remotes, + **kwargs, + ) + if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": + breakpoint() + validator.validate(instance=self.data) + + def validate_ignoring_errors(self, Validator): # pragma: no cover + with suppress(jsonschema.ValidationError): + self.validate(Validator=Validator) + + +def _someone_save_us_the_module_of_the_caller(): + """ + The FQON of the module 2nd stack frames up from here. + + This is intended to allow us to dynamically return test case classes that + are indistinguishable from being defined in the module that wants them. + + Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run + the class that really is running. + + Save us all, this is all so so so so so terrible. + """ + + return sys._getframe(2).f_globals["__name__"] diff --git a/jsonschema/tests/fuzz_validate.py b/jsonschema/tests/fuzz_validate.py new file mode 100644 index 0000000..c12e88b --- /dev/null +++ b/jsonschema/tests/fuzz_validate.py @@ -0,0 +1,50 @@ +""" +Fuzzing setup for OSS-Fuzz. + +See https://github.com/google/oss-fuzz/tree/master/projects/jsonschema for the +other half of the setup here. +""" +import sys + +from hypothesis import given, strategies + +import jsonschema + +PRIM = strategies.one_of( + strategies.booleans(), + strategies.integers(), + strategies.floats(allow_nan=False, allow_infinity=False), + strategies.text(), +) +DICT = strategies.recursive( + base=strategies.one_of( + strategies.booleans(), + strategies.dictionaries(strategies.text(), PRIM), + ), + extend=lambda inner: strategies.dictionaries(strategies.text(), inner), +) + + +@given(obj1=DICT, obj2=DICT) +def test_schemas(obj1, obj2): + try: + jsonschema.validate(instance=obj1, schema=obj2) + except jsonschema.exceptions.ValidationError: + pass + except jsonschema.exceptions.SchemaError: + pass + + +def main(): + atheris.instrument_all() + atheris.Setup( + sys.argv, + test_schemas.hypothesis.fuzz_one_input, + enable_python_coverage=True, + ) + atheris.Fuzz() + + +if __name__ == "__main__": + import atheris + main() diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py new file mode 100644 index 0000000..c5b0b2e --- /dev/null +++ b/jsonschema/tests/test_cli.py @@ -0,0 +1,916 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from json import JSONDecodeError +from pathlib import Path +from textwrap import dedent +from unittest import TestCase +import json +import os +import subprocess +import sys +import tempfile +import warnings + +try: # pragma: no cover + from importlib import metadata +except ImportError: # pragma: no cover + import importlib_metadata as metadata # type: ignore + +from jsonschema import Draft4Validator, Draft202012Validator +from jsonschema.exceptions import ( + SchemaError, + ValidationError, + _RefResolutionError, +) +from jsonschema.validators import _LATEST_VERSION, validate + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from jsonschema import cli + + +def fake_validator(*errors): + errors = list(reversed(errors)) + + class FakeValidator: + def __init__(self, *args, **kwargs): + pass + + def iter_errors(self, instance): + if errors: + return errors.pop() + return [] # pragma: no cover + + @classmethod + def check_schema(self, schema): + pass + + return FakeValidator + + +def fake_open(all_contents): + def open(path): + contents = all_contents.get(path) + if contents is None: + raise FileNotFoundError(path) + return StringIO(contents) + return open + + +def _message_for(non_json): + try: + json.loads(non_json) + except JSONDecodeError as error: + return str(error) + else: # pragma: no cover + raise RuntimeError("Tried and failed to capture a JSON dump error.") + + +class TestCLI(TestCase): + def run_cli( + self, argv, files=None, stdin=StringIO(), exit_code=0, **override, + ): + arguments = cli.parse_args(argv) + arguments.update(override) + + self.assertFalse(hasattr(cli, "open")) + cli.open = fake_open(files or {}) + try: + stdout, stderr = StringIO(), StringIO() + actual_exit_code = cli.run( + arguments, + stdin=stdin, + stdout=stdout, + stderr=stderr, + ) + finally: + del cli.open + + self.assertEqual( + actual_exit_code, exit_code, msg=dedent( + """ + Expected an exit code of {} != {}. + + stdout: {} + + stderr: {} + """.format( + exit_code, + actual_exit_code, + stdout.getvalue(), + stderr.getvalue(), + ), + ), + ) + return stdout.getvalue(), stderr.getvalue() + + def assertOutputs(self, stdout="", stderr="", **kwargs): + self.assertEqual( + self.run_cli(**kwargs), + (dedent(stdout), dedent(stderr)), + ) + + def test_invalid_instance(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="12: I am an error!\n", + ) + + def test_invalid_instance_pretty_output(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["-i", "some_instance", "--output", "pretty", "some_schema"], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_instance)=== + + I am an error! + ----------------------------- + """, + ) + + def test_invalid_instance_explicit_plain_output(self): + error = ValidationError("I am an error!", instance=12) + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(error.instance), + ), + validator=fake_validator([error]), + + argv=["--output", "plain", "-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="12: I am an error!\n", + ) + + def test_invalid_instance_multiple_errors(self): + instance = 12 + first = ValidationError("First error", instance=instance) + second = ValidationError("Second error", instance=instance) + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(instance), + ), + validator=fake_validator([first, second]), + + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 12: First error + 12: Second error + """, + ) + + def test_invalid_instance_multiple_errors_pretty_output(self): + instance = 12 + first = ValidationError("First error", instance=instance) + second = ValidationError("Second error", instance=instance) + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_instance=json.dumps(instance), + ), + validator=fake_validator([first, second]), + + argv=["-i", "some_instance", "--output", "pretty", "some_schema"], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_instance)=== + + First error + ----------------------------- + ===[ValidationError]===(some_instance)=== + + Second error + ----------------------------- + """, + ) + + def test_multiple_invalid_instances(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + 12: An error + 12: Another error + foo: BOOM + """, + ) + + def test_multiple_invalid_instances_pretty_output(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "--output", "pretty", + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + ===[ValidationError]===(some_first_instance)=== + + An error + ----------------------------- + ===[ValidationError]===(some_first_instance)=== + + Another error + ----------------------------- + ===[ValidationError]===(some_second_instance)=== + + BOOM + ----------------------------- + """, + ) + + def test_custom_error_format(self): + first_instance = 12 + first_errors = [ + ValidationError("An error", instance=first_instance), + ValidationError("Another error", instance=first_instance), + ] + second_instance = "foo" + second_errors = [ValidationError("BOOM", instance=second_instance)] + + self.assertOutputs( + files=dict( + some_schema='{"does not": "matter since it is stubbed"}', + some_first_instance=json.dumps(first_instance), + some_second_instance=json.dumps(second_instance), + ), + validator=fake_validator(first_errors, second_errors), + + argv=[ + "--error-format", ":{error.message}._-_.{error.instance}:", + "-i", "some_first_instance", + "-i", "some_second_instance", + "some_schema", + ], + + exit_code=1, + stderr=":An error._-_.12::Another error._-_.12::BOOM._-_.foo:", + ) + + def test_invalid_schema(self): + self.assertOutputs( + files=dict(some_schema='{"type": 12}'), + argv=["some_schema"], + + exit_code=1, + stderr="""\ + 12: 12 is not valid under any of the given schemas + """, + ) + + def test_invalid_schema_pretty_output(self): + schema = {"type": 12} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance="") + error = str(e.exception) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_schema_multiple_errors(self): + self.assertOutputs( + files=dict(some_schema='{"type": 12, "items": 57}'), + argv=["some_schema"], + + exit_code=1, + stderr="""\ + 57: 57 is not of type 'object', 'boolean' + """, + ) + + def test_invalid_schema_multiple_errors_pretty_output(self): + schema = {"type": 12, "items": 57} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance="") + error = str(e.exception) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_schema_with_invalid_instance(self): + """ + "Validating" an instance that's invalid under an invalid schema + just shows the schema error. + """ + self.assertOutputs( + files=dict( + some_schema='{"type": 12, "minimum": 30}', + some_instance="13", + ), + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 12: 12 is not valid under any of the given schemas + """, + ) + + def test_invalid_schema_with_invalid_instance_pretty_output(self): + instance, schema = 13, {"type": 12, "minimum": 30} + + with self.assertRaises(SchemaError) as e: + validate(schema=schema, instance=instance) + error = str(e.exception) + + self.assertOutputs( + files=dict( + some_schema=json.dumps(schema), + some_instance=json.dumps(instance), + ), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + stderr=( + "===[SchemaError]===(some_schema)===\n\n" + + str(error) + + "\n-----------------------------\n" + ), + ) + + def test_invalid_instance_continues_with_the_rest(self): + self.assertOutputs( + files=dict( + some_schema='{"minimum": 30}', + first_instance="not valid JSON!", + second_instance="12", + ), + argv=[ + "-i", "first_instance", + "-i", "second_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + Failed to parse 'first_instance': {} + 12: 12 is less than the minimum of 30 + """.format(_message_for("not valid JSON!")), + ) + + def test_custom_error_format_applies_to_schema_errors(self): + instance, schema = 13, {"type": 12, "minimum": 30} + + with self.assertRaises(SchemaError): + validate(schema=schema, instance=instance) + + self.assertOutputs( + files=dict(some_schema=json.dumps(schema)), + + argv=[ + "--error-format", ":{error.message}._-_.{error.instance}:", + "some_schema", + ], + + exit_code=1, + stderr=":12 is not valid under any of the given schemas._-_.12:", + ) + + def test_instance_is_invalid_JSON(self): + instance = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema="{}", some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + + exit_code=1, + stderr="""\ + Failed to parse 'some_instance': {} + """.format(_message_for(instance)), + ) + + def test_instance_is_invalid_JSON_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict( + some_schema="{}", + some_instance="not valid JSON!", + ), + + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_instance)===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_schema", stderr) + + def test_instance_is_invalid_JSON_on_stdin(self): + instance = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO(instance), + + argv=["some_schema"], + + exit_code=1, + stderr="""\ + Failed to parse <stdin>: {} + """.format(_message_for(instance)), + ) + + def test_instance_is_invalid_JSON_on_stdin_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict(some_schema="{}"), + stdin=StringIO("not valid JSON!"), + + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(<stdin>)===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_schema", stderr) + + def test_schema_is_invalid_JSON(self): + schema = "not valid JSON!" + + self.assertOutputs( + files=dict(some_schema=schema), + + argv=["some_schema"], + + exit_code=1, + stderr="""\ + Failed to parse 'some_schema': {} + """.format(_message_for(schema)), + ) + + def test_schema_is_invalid_JSON_pretty_output(self): + stdout, stderr = self.run_cli( + files=dict(some_schema="not valid JSON!"), + + argv=["--output", "pretty", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_schema)===\n\nTraceback (most recent call last):\n", + stderr, + ) + + def test_schema_and_instance_are_both_invalid_JSON(self): + """ + Only the schema error is reported, as we abort immediately. + """ + schema, instance = "not valid JSON!", "also not valid JSON!" + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + + argv=["some_schema"], + + exit_code=1, + stderr="""\ + Failed to parse 'some_schema': {} + """.format(_message_for(schema)), + ) + + def test_schema_and_instance_are_both_invalid_JSON_pretty_output(self): + """ + Only the schema error is reported, as we abort immediately. + """ + stdout, stderr = self.run_cli( + files=dict( + some_schema="not valid JSON!", + some_instance="also not valid JSON!", + ), + + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + + exit_code=1, + ) + self.assertFalse(stdout) + self.assertIn( + "(some_schema)===\n\nTraceback (most recent call last):\n", + stderr, + ) + self.assertNotIn("some_instance", stderr) + + def test_instance_does_not_exist(self): + self.assertOutputs( + files=dict(some_schema="{}"), + argv=["-i", "nonexisting_instance", "some_schema"], + + exit_code=1, + stderr="""\ + 'nonexisting_instance' does not exist. + """, + ) + + def test_instance_does_not_exist_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}"), + argv=[ + "--output", "pretty", + "-i", "nonexisting_instance", + "some_schema", + ], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_instance)=== + + 'nonexisting_instance' does not exist. + ----------------------------- + """, + ) + + def test_schema_does_not_exist(self): + self.assertOutputs( + argv=["nonexisting_schema"], + + exit_code=1, + stderr="'nonexisting_schema' does not exist.\n", + ) + + def test_schema_does_not_exist_pretty_output(self): + self.assertOutputs( + argv=["--output", "pretty", "nonexisting_schema"], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_schema)=== + + 'nonexisting_schema' does not exist. + ----------------------------- + """, + ) + + def test_neither_instance_nor_schema_exist(self): + self.assertOutputs( + argv=["-i", "nonexisting_instance", "nonexisting_schema"], + + exit_code=1, + stderr="'nonexisting_schema' does not exist.\n", + ) + + def test_neither_instance_nor_schema_exist_pretty_output(self): + self.assertOutputs( + argv=[ + "--output", "pretty", + "-i", "nonexisting_instance", + "nonexisting_schema", + ], + + exit_code=1, + stderr="""\ + ===[FileNotFoundError]===(nonexisting_schema)=== + + 'nonexisting_schema' does not exist. + ----------------------------- + """, + ) + + def test_successful_validation(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + stdout="===[SUCCESS]===(some_instance)===\n", + stderr="", + ) + + def test_successful_validation_of_stdin(self): + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO("{}"), + argv=["some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_of_stdin_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}"), + stdin=StringIO("{}"), + argv=["--output", "pretty", "some_schema"], + stdout="===[SUCCESS]===(<stdin>)===\n", + stderr="", + ) + + def test_successful_validation_of_just_the_schema(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + def test_successful_validation_of_just_the_schema_pretty_output(self): + self.assertOutputs( + files=dict(some_schema="{}", some_instance="{}"), + argv=["--output", "pretty", "-i", "some_instance", "some_schema"], + stdout="===[SUCCESS]===(some_instance)===\n", + stderr="", + ) + + def test_successful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + ref_schema_file.close() + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance="1"), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + stdout="", + stderr="", + ) + + def test_unsuccessful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + ref_schema_file.close() + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#/definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance='"1"'), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + exit_code=1, + stdout="", + stderr="1: '1' is not of type 'integer'\n", + ) + + def test_nonexistent_file_with_explicit_base_uri(self): + schema = '{"$ref": "someNonexistentFile.json#definitions/num"}' + instance = "1" + + with self.assertRaises(_RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", Path.cwd().as_uri(), + "some_schema", + ], + ) + error = str(e.exception) + self.assertIn(f"{os.sep}someNonexistentFile.json'", error) + + def test_invalid_explicit_base_uri(self): + schema = '{"$ref": "foo.json#definitions/num"}' + instance = "1" + + with self.assertRaises(_RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", "not@UR1", + "some_schema", + ], + ) + error = str(e.exception) + self.assertEqual( + error, "unknown url type: 'foo.json'", + ) + + def test_it_validates_using_the_latest_validator_when_unspecified(self): + # There isn't a better way now I can think of to ensure that the + # latest version was used, given that the call to validator_for + # is hidden inside the CLI, so guard that that's the case, and + # this test will have to be updated when versions change until + # we can think of a better way to ensure this behavior. + self.assertIs(Draft202012Validator, _LATEST_VERSION) + + self.assertOutputs( + files=dict(some_schema='{"const": "check"}', some_instance='"a"'), + argv=["-i", "some_instance", "some_schema"], + exit_code=1, + stdout="", + stderr="a: 'check' was expected\n", + ) + + def test_it_validates_using_draft7_when_specified(self): + """ + Specifically, `const` validation applies for Draft 7. + """ + schema = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "const": "check" + } + """ + instance = '"foo"' + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + exit_code=1, + stdout="", + stderr="foo: 'check' was expected\n", + ) + + def test_it_validates_using_draft4_when_specified(self): + """ + Specifically, `const` validation *does not* apply for Draft 4. + """ + schema = """ + { + "$schema": "http://json-schema.org/draft-04/schema#", + "const": "check" + } + """ + instance = '"foo"' + self.assertOutputs( + files=dict(some_schema=schema, some_instance=instance), + argv=["-i", "some_instance", "some_schema"], + stdout="", + stderr="", + ) + + +class TestParser(TestCase): + + FakeValidator = fake_validator() + + def test_find_validator_by_fully_qualified_object_name(self): + arguments = cli.parse_args( + [ + "--validator", + "jsonschema.tests.test_cli.TestParser.FakeValidator", + "--instance", "mem://some/instance", + "mem://some/schema", + ], + ) + self.assertIs(arguments["validator"], self.FakeValidator) + + def test_find_validator_in_jsonschema(self): + arguments = cli.parse_args( + [ + "--validator", "Draft4Validator", + "--instance", "mem://some/instance", + "mem://some/schema", + ], + ) + self.assertIs(arguments["validator"], Draft4Validator) + + def cli_output_for(self, *argv): + stdout, stderr = StringIO(), StringIO() + with redirect_stdout(stdout), redirect_stderr(stderr): + with self.assertRaises(SystemExit): + cli.parse_args(argv) + return stdout.getvalue(), stderr.getvalue() + + def test_unknown_output(self): + stdout, stderr = self.cli_output_for( + "--output", "foo", + "mem://some/schema", + ) + self.assertIn("invalid choice: 'foo'", stderr) + self.assertFalse(stdout) + + def test_useless_error_format(self): + stdout, stderr = self.cli_output_for( + "--output", "pretty", + "--error-format", "foo", + "mem://some/schema", + ) + self.assertIn( + "--error-format can only be used with --output plain", + stderr, + ) + self.assertFalse(stdout) + + +class TestCLIIntegration(TestCase): + def test_license(self): + output = subprocess.check_output( + [sys.executable, "-m", "pip", "show", "jsonschema"], + stderr=subprocess.STDOUT, + ) + self.assertIn(b"License: MIT", output) + + def test_version(self): + version = subprocess.check_output( + [sys.executable, "-W", "ignore", "-m", "jsonschema", "--version"], + stderr=subprocess.STDOUT, + ) + version = version.decode("utf-8").strip() + self.assertEqual(version, metadata.version("jsonschema")) + + def test_no_arguments_shows_usage_notes(self): + output = subprocess.check_output( + [sys.executable, "-m", "jsonschema"], + stderr=subprocess.STDOUT, + ) + output_for_help = subprocess.check_output( + [sys.executable, "-m", "jsonschema", "--help"], + stderr=subprocess.STDOUT, + ) + self.assertEqual(output, output_for_help) diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py new file mode 100644 index 0000000..2beb02f --- /dev/null +++ b/jsonschema/tests/test_deprecations.py @@ -0,0 +1,299 @@ +from unittest import TestCase +import importlib +import subprocess +import sys + +from jsonschema import FormatChecker, exceptions, validators + + +class TestDeprecations(TestCase): + def test_version(self): + """ + As of v4.0.0, __version__ is deprecated in favor of importlib.metadata. + """ + + message = "Accessing jsonschema.__version__ is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import __version__ # noqa + + self.assertEqual(w.filename, __file__) + + def test_validators_ErrorTree(self): + """ + As of v4.0.0, importing ErrorTree from jsonschema.validators is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing ErrorTree from jsonschema.validators is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.validators import ErrorTree # noqa + + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_import_ErrorTree(self): + """ + As of v4.18.0, importing ErrorTree from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing ErrorTree directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import ErrorTree # noqa + + self.assertEqual(ErrorTree, exceptions.ErrorTree) + self.assertEqual(w.filename, __file__) + + def test_import_FormatError(self): + """ + As of v4.18.0, importing FormatError from the package root is + deprecated in favor of doing so from jsonschema.exceptions. + """ + + message = "Importing FormatError directly from the jsonschema package " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import FormatError # noqa + + self.assertEqual(FormatError, exceptions.FormatError) + self.assertEqual(w.filename, __file__) + + def test_validators_validators(self): + """ + As of v4.0.0, accessing jsonschema.validators.validators is + deprecated. + """ + + message = "Accessing jsonschema.validators.validators is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + value = validators.validators + + self.assertEqual(value, validators._VALIDATORS) + self.assertEqual(w.filename, __file__) + + def test_validators_meta_schemas(self): + """ + As of v4.0.0, accessing jsonschema.validators.meta_schemas is + deprecated. + """ + + message = "Accessing jsonschema.validators.meta_schemas is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + value = validators.meta_schemas + + self.assertEqual(value, validators._META_SCHEMAS) + self.assertEqual(w.filename, __file__) + + def test_RefResolver_in_scope(self): + """ + As of v4.0.0, RefResolver.in_scope is deprecated. + """ + + resolver = validators._RefResolver.from_schema({}) + message = "jsonschema.RefResolver.in_scope is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + with resolver.in_scope("foo"): + pass + + self.assertEqual(w.filename, __file__) + + def test_Validator_is_valid_two_arguments(self): + """ + As of v4.0.0, calling is_valid with two arguments (to provide a + different schema) is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Passing a schema to Validator.is_valid is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + result = validator.is_valid("foo", {"type": "number"}) + + self.assertFalse(result) + self.assertEqual(w.filename, __file__) + + def test_Validator_iter_errors_two_arguments(self): + """ + As of v4.0.0, calling iter_errors with two arguments (to provide a + different schema) is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Passing a schema to Validator.iter_errors is deprecated " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + error, = validator.iter_errors("foo", {"type": "number"}) + + self.assertEqual(error.validator, "type") + self.assertEqual(w.filename, __file__) + + def test_Validator_resolver(self): + """ + As of v4.18.0, accessing Validator.resolver is deprecated. + """ + + validator = validators.Draft7Validator({}) + message = "Accessing Draft7Validator.resolver is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + self.assertIsInstance(validator.resolver, validators._RefResolver) + + self.assertEqual(w.filename, __file__) + + def test_RefResolver(self): + """ + As of v4.18.0, RefResolver is fully deprecated. + """ + + message = "jsonschema.RefResolver is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolver # noqa: F401 + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.validators import RefResolver # noqa: F401, F811 + self.assertEqual(w.filename, __file__) + + def test_RefResolutionError(self): + """ + As of v4.18.0, RefResolutionError is deprecated in favor of directly + catching errors from the referencing library. + """ + + message = "jsonschema.exceptions.RefResolutionError is deprecated" + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import RefResolutionError # noqa: F401 + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema.exceptions import RefResolutionError # noqa + + self.assertEqual(RefResolutionError, exceptions._RefResolutionError) + self.assertEqual(w.filename, __file__) + + def test_Validator_subclassing(self): + """ + As of v4.12.0, subclassing a validator class produces an explicit + deprecation warning. + + This was never intended to be public API (and some comments over the + years in issues said so, but obviously that's not a great way to make + sure it's followed). + + A future version will explicitly raise an error. + """ + + message = "Subclassing validator classes is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + class Subclass(validators.Draft202012Validator): + pass + + self.assertEqual(w.filename, __file__) + + with self.assertWarnsRegex(DeprecationWarning, message) as w: + class AnotherSubclass(validators.create(meta_schema={})): + pass + + def test_FormatChecker_cls_checks(self): + """ + As of v4.14.0, FormatChecker.cls_checks is deprecated without + replacement. + """ + + self.addCleanup(FormatChecker.checkers.pop, "boom", None) + + message = "FormatChecker.cls_checks " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + FormatChecker.cls_checks("boom") + + self.assertEqual(w.filename, __file__) + + def test_draftN_format_checker(self): + """ + As of v4.16.0, accessing jsonschema.draftn_format_checker is deprecated + in favor of Validator.FORMAT_CHECKER. + """ + + message = "Accessing jsonschema.draft202012_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft202012_format_checker # noqa + + self.assertIs( + draft202012_format_checker, + validators.Draft202012Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft201909_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft201909_format_checker # noqa + + self.assertIs( + draft201909_format_checker, + validators.Draft201909Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft7_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft7_format_checker # noqa + + self.assertIs( + draft7_format_checker, + validators.Draft7Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft6_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft6_format_checker # noqa + + self.assertIs( + draft6_format_checker, + validators.Draft6Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft4_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft4_format_checker # noqa + + self.assertIs( + draft4_format_checker, + validators.Draft4Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + message = "Accessing jsonschema.draft3_format_checker is " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + from jsonschema import draft3_format_checker # noqa + + self.assertIs( + draft3_format_checker, + validators.Draft3Validator.FORMAT_CHECKER, + ) + self.assertEqual(w.filename, __file__) + + with self.assertRaises(ImportError): + from jsonschema import draft1234_format_checker # noqa + + def test_import_cli(self): + """ + As of v4.17.0, importing jsonschema.cli is deprecated. + """ + + message = "The jsonschema CLI is deprecated and will be removed " + with self.assertWarnsRegex(DeprecationWarning, message) as w: + import jsonschema.cli + importlib.reload(jsonschema.cli) + + self.assertEqual(w.filename, importlib.__file__) + + def test_cli(self): + """ + As of v4.17.0, the jsonschema CLI is deprecated. + """ + + process = subprocess.run( + [sys.executable, "-m", "jsonschema"], + capture_output=True, + ) + self.assertIn(b"The jsonschema CLI is deprecated ", process.stderr) diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py new file mode 100644 index 0000000..00ff300 --- /dev/null +++ b/jsonschema/tests/test_exceptions.py @@ -0,0 +1,601 @@ +from unittest import TestCase +import textwrap + +from jsonschema import exceptions +from jsonschema.validators import _LATEST_VERSION + + +class TestBestMatch(TestCase): + def best_match_of(self, instance, schema): + errors = list(_LATEST_VERSION(schema).iter_errors(instance)) + best = exceptions.best_match(iter(errors)) + reversed_best = exceptions.best_match(reversed(errors)) + self.assertEqual( + best._contents(), + reversed_best._contents(), + f"No consistent best match!\nGot: {best}\n\nThen: {reversed_best}", + ) + return best + + def test_shallower_errors_are_better_matches(self): + schema = { + "properties": { + "foo": { + "minProperties": 2, + "properties": {"bar": {"type": "object"}}, + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": []}}, schema=schema) + self.assertEqual(best.validator, "minProperties") + + def test_oneOf_and_anyOf_are_weak_matches(self): + """ + A property you *must* match is probably better than one you have to + match a part of. + """ + + schema = { + "minProperties": 2, + "anyOf": [{"type": "string"}, {"type": "number"}], + "oneOf": [{"type": "string"}, {"type": "number"}], + } + best = self.best_match_of(instance={}, schema=schema) + self.assertEqual(best.validator, "minProperties") + + def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): + """ + If the most relevant error is an anyOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + """ + + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_no_anyOf_traversal_for_equally_relevant_errors(self): + """ + We don't traverse into an anyOf (as above) if all of its context errors + seem to be equally "wrong" against the instance. + """ + + schema = { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + {"type": "object"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "anyOf") + + def test_anyOf_traversal_for_single_equally_relevant_error(self): + """ + We *do* traverse anyOf with a single nested error, even though it is + vacuously equally relevant to itself. + """ + + schema = { + "anyOf": [ + {"type": "string"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "type") + + def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): + """ + If the most relevant error is an oneOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + """ + + schema = { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_no_oneOf_traversal_for_equally_relevant_errors(self): + """ + We don't traverse into an oneOf (as above) if all of its context errors + seem to be equally "wrong" against the instance. + """ + + schema = { + "oneOf": [ + {"type": "string"}, + {"type": "integer"}, + {"type": "object"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "oneOf") + + def test_oneOf_traversal_for_single_equally_relevant_error(self): + """ + We *do* traverse oneOf with a single nested error, even though it is + vacuously equally relevant to itself. + """ + + schema = { + "oneOf": [ + {"type": "string"}, + ], + } + best = self.best_match_of(instance=[], schema=schema) + self.assertEqual(best.validator, "type") + + def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): + """ + Now, if the error is allOf, we traverse but select the *most* relevant + error from the context, because all schemas here must match anyways. + """ + + schema = { + "properties": { + "foo": { + "allOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "string") + + def test_nested_context_for_oneOf(self): + """ + We traverse into nested contexts (a oneOf containing an error in a + nested oneOf here). + """ + + schema = { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + { + "oneOf": [ + {"type": "string"}, + { + "properties": { + "bar": {"type": "array"}, + }, + }, + ], + }, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) + self.assertEqual(best.validator_value, "array") + + def test_it_prioritizes_matching_types(self): + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": "array", "minItems": 2}, + {"type": "string", "minLength": 10}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertEqual(best.validator, "minLength") + + reordered = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string", "minLength": 10}, + {"type": "array", "minItems": 2}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=reordered) + self.assertEqual(best.validator, "minLength") + + def test_it_prioritizes_matching_union_types(self): + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": ["array", "object"], "minItems": 2}, + {"type": ["integer", "string"], "minLength": 10}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertEqual(best.validator, "minLength") + + reordered = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string", "minLength": 10}, + {"type": "array", "minItems": 2}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=reordered) + self.assertEqual(best.validator, "minLength") + + def test_boolean_schemas(self): + schema = {"properties": {"foo": False}} + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertIsNone(best.validator) + + def test_one_error(self): + validator = _LATEST_VERSION({"minProperties": 2}) + error, = validator.iter_errors({}) + self.assertEqual( + exceptions.best_match(validator.iter_errors({})).validator, + "minProperties", + ) + + def test_no_errors(self): + validator = _LATEST_VERSION({}) + self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) + + +class TestByRelevance(TestCase): + def test_short_paths_are_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=["baz"]) + deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) + match = max([shallow, deep], key=exceptions.relevance) + self.assertIs(match, shallow) + + match = max([deep, shallow], key=exceptions.relevance) + self.assertIs(match, shallow) + + def test_global_errors_are_even_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=[]) + deep = exceptions.ValidationError("Oh yes!", path=["foo"]) + + errors = sorted([shallow, deep], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + errors = sorted([deep, shallow], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + def test_weak_keywords_are_lower_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + + best_match = exceptions.by_relevance(weak="a") + + match = max([weak, normal], key=best_match) + self.assertIs(match, normal) + + match = max([normal, weak], key=best_match) + self.assertIs(match, normal) + + def test_strong_keywords_are_higher_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") + + best_match = exceptions.by_relevance(weak="a", strong="c") + + match = max([weak, normal, strong], key=best_match) + self.assertIs(match, strong) + + match = max([strong, normal, weak], key=best_match) + self.assertIs(match, strong) + + +class TestErrorTree(TestCase): + def test_it_knows_how_many_total_errors_it_contains(self): + # FIXME: #442 + errors = [ + exceptions.ValidationError("Something", validator=i) + for i in range(8) + ] + tree = exceptions.ErrorTree(errors) + self.assertEqual(tree.total_errors, 8) + + def test_it_contains_an_item_if_the_item_had_an_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertIn("bar", tree) + + def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertNotIn("foo", tree) + + def test_keywords_that_failed_appear_in_errors_dict(self): + error = exceptions.ValidationError("a message", validator="foo") + tree = exceptions.ErrorTree([error]) + self.assertEqual(tree.errors, {"foo": error}) + + def test_it_creates_a_child_tree_for_each_nested_path(self): + errors = [ + exceptions.ValidationError("a bar message", path=["bar"]), + exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]), + ] + tree = exceptions.ErrorTree(errors) + self.assertIn(0, tree["bar"]) + self.assertNotIn(1, tree["bar"]) + + def test_children_have_their_errors_dicts_built(self): + e1, e2 = ( + exceptions.ValidationError("1", validator="foo", path=["bar", 0]), + exceptions.ValidationError("2", validator="quux", path=["bar", 0]), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2}) + + def test_multiple_errors_with_instance(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + exceptions.ErrorTree([e1, e2]) + + def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): + error = exceptions.ValidationError("123", validator="foo", instance=[]) + tree = exceptions.ErrorTree([error]) + + with self.assertRaises(IndexError): + tree[0] + + def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): + """ + If a keyword refers to a path that isn't in the instance, the + tree still properly returns a subtree for that path. + """ + + error = exceptions.ValidationError( + "a message", validator="foo", instance={}, path=["foo"], + ) + tree = exceptions.ErrorTree([error]) + self.assertIsInstance(tree["foo"], exceptions.ErrorTree) + + def test_repr_single(self): + error = exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1", + ) + tree = exceptions.ErrorTree([error]) + self.assertEqual(repr(tree), "<ErrorTree (1 total error)>") + + def test_repr_multiple(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(repr(tree), "<ErrorTree (2 total errors)>") + + def test_repr_empty(self): + tree = exceptions.ErrorTree([]) + self.assertEqual(repr(tree), "<ErrorTree (0 total errors)>") + + +class TestErrorInitReprStr(TestCase): + def make_error(self, **kwargs): + defaults = dict( + message="hello", + validator="type", + validator_value="string", + instance=5, + schema={"type": "string"}, + ) + defaults.update(kwargs) + return exceptions.ValidationError(**defaults) + + def assertShows(self, expected, **kwargs): + expected = textwrap.dedent(expected).rstrip("\n") + + error = self.make_error(**kwargs) + message_line, _, rest = str(error).partition("\n") + self.assertEqual(message_line, error.message) + self.assertEqual(rest, expected) + + def test_it_calls_super_and_sets_args(self): + error = self.make_error() + self.assertGreater(len(error.args), 1) + + def test_repr(self): + self.assertEqual( + repr(exceptions.ValidationError(message="Hello!")), + "<ValidationError: 'Hello!'>", + ) + + def test_unset_error(self): + error = exceptions.ValidationError("message") + self.assertEqual(str(error), "message") + + kwargs = { + "validator": "type", + "validator_value": "string", + "instance": 5, + "schema": {"type": "string"}, + } + # Just the message should show if any of the attributes are unset + for attr in kwargs: + k = dict(kwargs) + del k[attr] + error = exceptions.ValidationError("message", **k) + self.assertEqual(str(error), "message") + + def test_empty_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'type': 'string'} + + On instance: + 5 + """, + path=[], + schema_path=[], + ) + + def test_one_item_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema: + {'type': 'string'} + + On instance[0]: + 5 + """, + path=[0], + schema_path=["items"], + ) + + def test_multiple_item_paths(self): + self.assertShows( + """ + Failed validating 'type' in schema['items'][0]: + {'type': 'string'} + + On instance[0]['a']: + 5 + """, + path=[0, "a"], + schema_path=["items", 0, 1], + ) + + def test_uses_pprint(self): + self.assertShows( + """ + Failed validating 'maxLength' in schema: + {0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + 15: 15, + 16: 16, + 17: 17, + 18: 18, + 19: 19} + + On instance: + [0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24] + """, + instance=list(range(25)), + schema=dict(zip(range(20), range(20))), + validator="maxLength", + ) + + def test_str_works_with_instances_having_overriden_eq_operator(self): + """ + Check for #164 which rendered exceptions unusable when a + `ValidationError` involved instances with an `__eq__` method + that returned truthy values. + """ + + class DontEQMeBro: + def __eq__(this, other): # pragma: no cover + self.fail("Don't!") + + def __ne__(this, other): # pragma: no cover + self.fail("Don't!") + + instance = DontEQMeBro() + error = exceptions.ValidationError( + "a message", + validator="foo", + instance=instance, + validator_value="some", + schema="schema", + ) + self.assertIn(repr(instance), str(error)) + + +class TestHashable(TestCase): + def test_hashable(self): + set([exceptions.ValidationError("")]) + set([exceptions.SchemaError("")]) diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py new file mode 100644 index 0000000..a5a1d0c --- /dev/null +++ b/jsonschema/tests/test_format.py @@ -0,0 +1,109 @@ +""" +Tests for the parts of jsonschema related to the :kw:`format` keyword. +""" + +from unittest import TestCase + +from jsonschema import FormatChecker, ValidationError +from jsonschema.exceptions import FormatError +from jsonschema.validators import Draft4Validator + +BOOM = ValueError("Boom!") +BANG = ZeroDivisionError("Bang!") + + +def boom(thing): + if thing == "bang": + raise BANG + raise BOOM + + +class TestFormatChecker(TestCase): + def test_it_can_validate_no_formats(self): + checker = FormatChecker(formats=()) + self.assertFalse(checker.checkers) + + def test_it_raises_a_key_error_for_unknown_formats(self): + with self.assertRaises(KeyError): + FormatChecker(formats=["o noes"]) + + def test_it_can_register_cls_checkers(self): + original = dict(FormatChecker.checkers) + self.addCleanup(FormatChecker.checkers.pop, "boom") + with self.assertWarns(DeprecationWarning): + FormatChecker.cls_checks("boom")(boom) + self.assertEqual( + FormatChecker.checkers, + dict(original, boom=(boom, ())), + ) + + def test_it_can_register_checkers(self): + checker = FormatChecker() + checker.checks("boom")(boom) + self.assertEqual( + checker.checkers, + dict(FormatChecker.checkers, boom=(boom, ())), + ) + + def test_it_catches_registered_errors(self): + checker = FormatChecker() + checker.checks("boom", raises=type(BOOM))(boom) + + with self.assertRaises(FormatError) as cm: + checker.check(instance=12, format="boom") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + + # Unregistered errors should not be caught + with self.assertRaises(type(BANG)): + checker.check(instance="bang", format="boom") + + def test_format_error_causes_become_validation_error_causes(self): + checker = FormatChecker() + checker.checks("boom", raises=ValueError)(boom) + validator = Draft4Validator({"format": "boom"}, format_checker=checker) + + with self.assertRaises(ValidationError) as cm: + validator.validate("BOOM") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + + def test_format_checkers_come_with_defaults(self): + # This is bad :/ but relied upon. + # The docs for quite awhile recommended people do things like + # validate(..., format_checker=FormatChecker()) + # We should change that, but we can't without deprecation... + checker = FormatChecker() + with self.assertRaises(FormatError): + checker.check(instance="not-an-ipv4", format="ipv4") + + def test_repr(self): + checker = FormatChecker(formats=()) + checker.checks("foo")(lambda thing: True) # pragma: no cover + checker.checks("bar")(lambda thing: True) # pragma: no cover + checker.checks("baz")(lambda thing: True) # pragma: no cover + self.assertEqual( + repr(checker), + "<FormatChecker checkers=['bar', 'baz', 'foo']>", + ) + + def test_duration_format(self): + try: + from jsonschema._format import is_duration # noqa: F401 + except ImportError: # pragma: no cover + pass + else: + checker = FormatChecker() + self.assertTrue(checker.conforms(1, "duration")) + self.assertTrue(checker.conforms("P4Y", "duration")) + self.assertFalse(checker.conforms("test", "duration")) + + def test_uuid_format(self): + checker = FormatChecker() + self.assertTrue(checker.conforms(1, "uuid")) + self.assertTrue( + checker.conforms("6e6659ec-4503-4428-9f03-2e2ea4d6c278", "uuid"), + ) + self.assertFalse(checker.conforms("test", "uuid")) diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py new file mode 100644 index 0000000..9c63714 --- /dev/null +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -0,0 +1,257 @@ +""" +Test runner for the JSON Schema official test suite + +Tests comprehensive correctness of each draft's validator. + +See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details. +""" + +import sys + +from jsonschema.tests._suite import Suite +import jsonschema + +SUITE = Suite() +DRAFT3 = SUITE.version(name="draft3") +DRAFT4 = SUITE.version(name="draft4") +DRAFT6 = SUITE.version(name="draft6") +DRAFT7 = SUITE.version(name="draft7") +DRAFT201909 = SUITE.version(name="draft2019-09") +DRAFT202012 = SUITE.version(name="draft2020-12") + + +def skip(message, **kwargs): + def skipper(test): + if all(value == getattr(test, attr) for attr, value in kwargs.items()): + return message + return skipper + + +def missing_format(Validator): + def missing_format(test): # pragma: no cover + schema = test.schema + if ( + schema is True + or schema is False + or "format" not in schema + or schema["format"] in Validator.FORMAT_CHECKER.checkers + or test.valid + ): + return + + return f"Format checker {schema['format']!r} not found." + return missing_format + + +def complex_email_validation(test): + if test.subject != "email": + return + + message = "Complex email validation is (intentionally) unsupported." + return skip( + message=message, + description="an invalid domain", + )(test) or skip( + message=message, + description="an invalid IPv4-address-literal", + )(test) or skip( + message=message, + description="dot after local part is not valid", + )(test) or skip( + message=message, + description="dot before local part is not valid", + )(test) or skip( + message=message, + description="two subsequent dots inside local part are not valid", + )(test) + + +if sys.version_info < (3, 9): # pragma: no cover + message = "Rejecting leading zeros is 3.9+" + allowed_leading_zeros = skip( + message=message, + subject="ipv4", + description="invalid leading zeroes, as they are treated as octals", + ) +else: + def allowed_leading_zeros(test): # pragma: no cover + return + + +def leap_second(test): + message = "Leap seconds are unsupported." + return skip( + message=message, + subject="time", + description="a valid time string with leap second", + )(test) or skip( + message=message, + subject="time", + description="a valid time string with leap second, Zulu", + )(test) or skip( + message=message, + subject="time", + description="a valid time string with leap second with offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, positive time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, negative time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, large positive time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, large negative time-offset", + )(test) or skip( + message=message, + subject="time", + description="valid leap second, zero time-offset", + )(test) or skip( + message=message, + subject="date-time", + description="a valid date-time with a leap second, UTC", + )(test) or skip( + message=message, + subject="date-time", + description="a valid date-time with a leap second, with minus offset", + )(test) + + +TestDraft3 = DRAFT3.to_unittest_testcase( + DRAFT3.cases(), + DRAFT3.format_cases(), + DRAFT3.optional_cases_of(name="bignum"), + DRAFT3.optional_cases_of(name="non-bmp-regex"), + DRAFT3.optional_cases_of(name="zeroTerminatedFloats"), + Validator=jsonschema.Draft3Validator, + format_checker=jsonschema.Draft3Validator.FORMAT_CHECKER, + skip=lambda test: ( + missing_format(jsonschema.Draft3Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft4 = DRAFT4.to_unittest_testcase( + DRAFT4.cases(), + DRAFT4.format_cases(), + DRAFT4.optional_cases_of(name="bignum"), + DRAFT4.optional_cases_of(name="float-overflow"), + DRAFT4.optional_cases_of(name="non-bmp-regex"), + DRAFT4.optional_cases_of(name="zeroTerminatedFloats"), + Validator=jsonschema.Draft4Validator, + format_checker=jsonschema.Draft4Validator.FORMAT_CHECKER, + skip=lambda test: ( + allowed_leading_zeros(test) + or leap_second(test) + or missing_format(jsonschema.Draft4Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft6 = DRAFT6.to_unittest_testcase( + DRAFT6.cases(), + DRAFT6.format_cases(), + DRAFT6.optional_cases_of(name="bignum"), + DRAFT6.optional_cases_of(name="float-overflow"), + DRAFT6.optional_cases_of(name="non-bmp-regex"), + Validator=jsonschema.Draft6Validator, + format_checker=jsonschema.Draft6Validator.FORMAT_CHECKER, + skip=lambda test: ( + allowed_leading_zeros(test) + or leap_second(test) + or missing_format(jsonschema.Draft6Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft7 = DRAFT7.to_unittest_testcase( + DRAFT7.cases(), + DRAFT7.format_cases(), + DRAFT7.optional_cases_of(name="bignum"), + DRAFT7.optional_cases_of(name="cross-draft"), + DRAFT7.optional_cases_of(name="float-overflow"), + DRAFT7.optional_cases_of(name="non-bmp-regex"), + Validator=jsonschema.Draft7Validator, + format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER, + skip=lambda test: ( + allowed_leading_zeros(test) + or leap_second(test) + or missing_format(jsonschema.Draft7Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft201909 = DRAFT201909.to_unittest_testcase( + DRAFT201909.cases(), + DRAFT201909.optional_cases_of(name="bignum"), + DRAFT201909.optional_cases_of(name="cross-draft"), + DRAFT201909.optional_cases_of(name="float-overflow"), + DRAFT201909.optional_cases_of(name="non-bmp-regex"), + DRAFT201909.optional_cases_of(name="refOfUnknownKeyword"), + Validator=jsonschema.Draft201909Validator, + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), + ), +) + + +TestDraft201909Format = DRAFT201909.to_unittest_testcase( + DRAFT201909.format_cases(), + name="TestDraft201909Format", + Validator=jsonschema.Draft201909Validator, + format_checker=jsonschema.Draft201909Validator.FORMAT_CHECKER, + skip=lambda test: ( + complex_email_validation(test) + or allowed_leading_zeros(test) + or leap_second(test) + or missing_format(jsonschema.Draft201909Validator)(test) + or complex_email_validation(test) + ), +) + + +TestDraft202012 = DRAFT202012.to_unittest_testcase( + DRAFT202012.cases(), + DRAFT202012.optional_cases_of(name="bignum"), + DRAFT202012.optional_cases_of(name="cross-draft"), + DRAFT202012.optional_cases_of(name="float-overflow"), + DRAFT202012.optional_cases_of(name="non-bmp-regex"), + DRAFT202012.optional_cases_of(name="refOfUnknownKeyword"), + Validator=jsonschema.Draft202012Validator, + skip=skip( + message="Vocabulary support is still in-progress.", + subject="vocabulary", + description=( + "no validation: invalid number, but it still validates" + ), + ), +) + + +TestDraft202012Format = DRAFT202012.to_unittest_testcase( + DRAFT202012.format_cases(), + name="TestDraft202012Format", + Validator=jsonschema.Draft202012Validator, + format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER, + skip=lambda test: ( + complex_email_validation(test) + or allowed_leading_zeros(test) + or leap_second(test) + or missing_format(jsonschema.Draft202012Validator)(test) + or complex_email_validation(test) + ), +) diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py new file mode 100644 index 0000000..3eacc72 --- /dev/null +++ b/jsonschema/tests/test_types.py @@ -0,0 +1,221 @@ +""" +Tests for the `TypeChecker`-based type interface. + +The actual correctness of the type checking is handled in +`test_jsonschema_test_suite`; these tests check that TypeChecker +functions correctly at a more granular level. +""" +from collections import namedtuple +from unittest import TestCase + +from jsonschema import ValidationError, _validators +from jsonschema._types import TypeChecker +from jsonschema.exceptions import UndefinedTypeCheck, UnknownType +from jsonschema.validators import Draft202012Validator, extend + + +def equals_2(checker, instance): + return instance == 2 + + +def is_namedtuple(instance): + return isinstance(instance, tuple) and getattr(instance, "_fields", None) + + +def is_object_or_named_tuple(checker, instance): + if Draft202012Validator.TYPE_CHECKER.is_type(instance, "object"): + return True + return is_namedtuple(instance) + + +class TestTypeChecker(TestCase): + def test_is_type(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual( + ( + checker.is_type(instance=2, type="two"), + checker.is_type(instance="bar", type="two"), + ), + (True, False), + ) + + def test_is_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as e: + TypeChecker().is_type(4, "foobar") + self.assertIn( + "'foobar' is unknown to this type checker", + str(e.exception), + ) + self.assertTrue( + e.exception.__suppress_context__, + msg="Expected the internal KeyError to be hidden.", + ) + + def test_checks_can_be_added_at_init(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual(checker, TypeChecker().redefine("two", equals_2)) + + def test_redefine_existing_type(self): + self.assertEqual( + TypeChecker().redefine("two", object()).redefine("two", equals_2), + TypeChecker().redefine("two", equals_2), + ) + + def test_remove(self): + self.assertEqual( + TypeChecker({"two": equals_2}).remove("two"), + TypeChecker(), + ) + + def test_remove_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as context: + TypeChecker().remove("foobar") + self.assertIn("foobar", str(context.exception)) + + def test_redefine_many(self): + self.assertEqual( + TypeChecker().redefine_many({"foo": int, "bar": str}), + TypeChecker().redefine("foo", int).redefine("bar", str), + ) + + def test_remove_multiple(self): + self.assertEqual( + TypeChecker({"foo": int, "bar": str}).remove("foo", "bar"), + TypeChecker(), + ) + + def test_type_check_can_raise_key_error(self): + """ + Make sure no one writes: + + try: + self._type_checkers[type](...) + except KeyError: + + ignoring the fact that the function itself can raise that. + """ + + error = KeyError("Stuff") + + def raises_keyerror(checker, instance): + raise error + + with self.assertRaises(KeyError) as context: + TypeChecker({"foo": raises_keyerror}).is_type(4, "foo") + + self.assertIs(context.exception, error) + + def test_repr(self): + checker = TypeChecker({"foo": is_namedtuple, "bar": is_namedtuple}) + self.assertEqual(repr(checker), "<TypeChecker types={'bar', 'foo'}>") + + +class TestCustomTypes(TestCase): + def test_simple_type_can_be_extended(self): + def int_or_str_int(checker, instance): + if not isinstance(instance, (int, str)): + return False + try: + int(instance) + except ValueError: + return False + return True + + CustomValidator = extend( + Draft202012Validator, + type_checker=Draft202012Validator.TYPE_CHECKER.redefine( + "integer", int_or_str_int, + ), + ) + validator = CustomValidator({"type": "integer"}) + + validator.validate(4) + validator.validate("4") + + with self.assertRaises(ValidationError): + validator.validate(4.4) + + with self.assertRaises(ValidationError): + validator.validate("foo") + + def test_object_can_be_extended(self): + schema = {"type": "object"} + + Point = namedtuple("Point", ["x", "y"]) + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + ) + validator = CustomValidator(schema) + + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_require_custom_validators(self): + schema = {"type": "object", "required": ["x"]} + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + ) + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Cannot handle required + with self.assertRaises(ValidationError): + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_can_handle_custom_validators(self): + schema = { + "type": "object", + "required": ["x"], + "properties": {"x": {"type": "integer"}}, + } + + type_checker = Draft202012Validator.TYPE_CHECKER.redefine( + "object", is_object_or_named_tuple, + ) + + def coerce_named_tuple(fn): + def coerced(validator, value, instance, schema): + if is_namedtuple(instance): + instance = instance._asdict() + return fn(validator, value, instance, schema) + return coerced + + required = coerce_named_tuple(_validators.required) + properties = coerce_named_tuple(_validators.properties) + + CustomValidator = extend( + Draft202012Validator, + type_checker=type_checker, + validators={"required": required, "properties": properties}, + ) + + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Can now process required and properties + validator.validate(Point(x=4, y=5)) + + with self.assertRaises(ValidationError): + validator.validate(Point(x="not an integer", y=5)) + + # As well as still handle objects. + validator.validate({"x": 4, "y": 5}) + + with self.assertRaises(ValidationError): + validator.validate({"x": "not an integer", "y": 5}) + + def test_unknown_type(self): + with self.assertRaises(UnknownType) as e: + Draft202012Validator({}).is_type(12, "some unknown type") + self.assertIn("'some unknown type'", str(e.exception)) diff --git a/jsonschema/tests/test_utils.py b/jsonschema/tests/test_utils.py new file mode 100644 index 0000000..4e542b9 --- /dev/null +++ b/jsonschema/tests/test_utils.py @@ -0,0 +1,124 @@ +from unittest import TestCase + +from jsonschema._utils import equal + + +class TestEqual(TestCase): + def test_none(self): + self.assertTrue(equal(None, None)) + + +class TestDictEqual(TestCase): + def test_equal_dictionaries(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "b"} + self.assertTrue(equal(dict_1, dict_2)) + + def test_missing_key(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "x": "b"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_additional_key(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "b", "x": "x"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_missing_value(self): + dict_1 = {"a": "b", "c": "d"} + dict_2 = {"c": "d", "a": "x"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_empty_dictionaries(self): + dict_1 = {} + dict_2 = {} + self.assertTrue(equal(dict_1, dict_2)) + + def test_one_none(self): + dict_1 = None + dict_2 = {"a": "b", "c": "d"} + self.assertFalse(equal(dict_1, dict_2)) + + def test_same_item(self): + dict_1 = {"a": "b", "c": "d"} + self.assertTrue(equal(dict_1, dict_1)) + + def test_nested_equal(self): + dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"} + dict_2 = {"c": "d", "a": {"a": "b", "c": "d"}} + self.assertTrue(equal(dict_1, dict_2)) + + def test_nested_dict_unequal(self): + dict_1 = {"a": {"a": "b", "c": "d"}, "c": "d"} + dict_2 = {"c": "d", "a": {"a": "b", "c": "x"}} + self.assertFalse(equal(dict_1, dict_2)) + + def test_mixed_nested_equal(self): + dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"} + dict_2 = {"c": "d", "a": ["a", "b", "c", "d"]} + self.assertTrue(equal(dict_1, dict_2)) + + def test_nested_list_unequal(self): + dict_1 = {"a": ["a", "b", "c", "d"], "c": "d"} + dict_2 = {"c": "d", "a": ["b", "c", "d", "a"]} + self.assertFalse(equal(dict_1, dict_2)) + + +class TestListEqual(TestCase): + def test_equal_lists(self): + list_1 = ["a", "b", "c"] + list_2 = ["a", "b", "c"] + self.assertTrue(equal(list_1, list_2)) + + def test_unsorted_lists(self): + list_1 = ["a", "b", "c"] + list_2 = ["b", "b", "a"] + self.assertFalse(equal(list_1, list_2)) + + def test_first_list_larger(self): + list_1 = ["a", "b", "c"] + list_2 = ["a", "b"] + self.assertFalse(equal(list_1, list_2)) + + def test_second_list_larger(self): + list_1 = ["a", "b"] + list_2 = ["a", "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + def test_list_with_none_unequal(self): + list_1 = ["a", "b", None] + list_2 = ["a", "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + list_1 = ["a", "b", None] + list_2 = [None, "b", "c"] + self.assertFalse(equal(list_1, list_2)) + + def test_list_with_none_equal(self): + list_1 = ["a", None, "c"] + list_2 = ["a", None, "c"] + self.assertTrue(equal(list_1, list_2)) + + def test_empty_list(self): + list_1 = [] + list_2 = [] + self.assertTrue(equal(list_1, list_2)) + + def test_one_none(self): + list_1 = None + list_2 = [] + self.assertFalse(equal(list_1, list_2)) + + def test_same_list(self): + list_1 = ["a", "b", "c"] + self.assertTrue(equal(list_1, list_1)) + + def test_equal_nested_lists(self): + list_1 = ["a", ["b", "c"], "d"] + list_2 = ["a", ["b", "c"], "d"] + self.assertTrue(equal(list_1, list_2)) + + def test_unequal_nested_lists(self): + list_1 = ["a", ["b", "c"], "d"] + list_2 = ["a", [], "c"] + self.assertFalse(equal(list_1, list_2)) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py new file mode 100644 index 0000000..a9b7f6a --- /dev/null +++ b/jsonschema/tests/test_validators.py @@ -0,0 +1,2359 @@ +from __future__ import annotations + +from collections import deque, namedtuple +from contextlib import contextmanager +from decimal import Decimal +from io import BytesIO +from unittest import TestCase, mock +from urllib.request import pathname2url +import json +import os +import sys +import tempfile +import warnings + +import attr + +from jsonschema import ( + FormatChecker, + TypeChecker, + exceptions, + protocols, + validators, +) + + +def fail(validator, errors, instance, schema): + for each in errors: + each.setdefault("message", "You told me to fail!") + yield exceptions.ValidationError(**each) + + +class TestCreateAndExtend(TestCase): + def setUp(self): + self.addCleanup( + self.assertEqual, + validators._META_SCHEMAS, + dict(validators._META_SCHEMAS), + ) + self.addCleanup( + self.assertEqual, + validators._VALIDATORS, + dict(validators._VALIDATORS), + ) + + self.meta_schema = {"$id": "some://meta/schema"} + self.validators = {"fail": fail} + self.type_checker = TypeChecker() + self.Validator = validators.create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker, + ) + + def test_attrs(self): + self.assertEqual( + ( + self.Validator.VALIDATORS, + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + ), ( + self.validators, + self.meta_schema, + self.type_checker, + ), + ) + + def test_init(self): + schema = {"fail": []} + self.assertEqual(self.Validator(schema).schema, schema) + + def test_iter_errors_successful(self): + schema = {"fail": []} + validator = self.Validator(schema) + + errors = list(validator.iter_errors("hello")) + self.assertEqual(errors, []) + + def test_iter_errors_one_error(self): + schema = {"fail": [{"message": "Whoops!"}]} + validator = self.Validator(schema) + + expected_error = exceptions.ValidationError( + "Whoops!", + instance="goodbye", + schema=schema, + validator="fail", + validator_value=[{"message": "Whoops!"}], + schema_path=deque(["fail"]), + ) + + errors = list(validator.iter_errors("goodbye")) + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0]._contents(), expected_error._contents()) + + def test_iter_errors_multiple_errors(self): + schema = { + "fail": [ + {"message": "First"}, + {"message": "Second!", "validator": "asdf"}, + {"message": "Third"}, + ], + } + validator = self.Validator(schema) + + errors = list(validator.iter_errors("goodbye")) + self.assertEqual(len(errors), 3) + + def test_if_a_version_is_provided_it_is_registered(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual(Validator.__name__, "MyVersionValidator") + self.assertEqual(Validator.__qualname__, "MyVersionValidator") + + def test_repr(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual( + repr(Validator({})), + "MyVersionValidator(schema={}, format_checker=None)", + ) + + def test_long_repr(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "my version") + self.assertEqual( + repr(Validator({"a": list(range(1000))})), ( + "MyVersionValidator(schema={'a': [0, 1, 2, 3, 4, 5, ...]}, " + "format_checker=None)" + ), + ) + + def test_repr_no_version(self): + Validator = validators.create(meta_schema={}) + self.assertEqual( + repr(Validator({})), + "Validator(schema={}, format_checker=None)", + ) + + def test_dashes_are_stripped_from_validator_names(self): + Validator = validators.create( + meta_schema={"$id": "something"}, + version="foo-bar", + ) + self.addCleanup(validators._META_SCHEMAS.pop, "something") + self.addCleanup(validators._VALIDATORS.pop, "foo-bar") + self.assertEqual(Validator.__qualname__, "FooBarValidator") + + def test_if_a_version_is_not_provided_it_is_not_registered(self): + original = dict(validators._META_SCHEMAS) + validators.create(meta_schema={"id": "id"}) + self.assertEqual(validators._META_SCHEMAS, original) + + def test_validates_registers_meta_schema_id(self): + meta_schema_key = "meta schema id" + my_meta_schema = {"id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + id_of=lambda s: s.get("id", ""), + ) + self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) + self.addCleanup(validators._VALIDATORS.pop, "my version") + + self.assertIn(meta_schema_key, validators._META_SCHEMAS) + + def test_validates_registers_meta_schema_draft6_id(self): + meta_schema_key = "meta schema $id" + my_meta_schema = {"$id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + ) + self.addCleanup(validators._META_SCHEMAS.pop, meta_schema_key) + self.addCleanup(validators._VALIDATORS.pop, "my version") + + self.assertIn(meta_schema_key, validators._META_SCHEMAS) + + def test_create_default_types(self): + Validator = validators.create(meta_schema={}, validators=()) + self.assertTrue( + all( + Validator({}).is_type(instance=instance, type=type) + for type, instance in [ + ("array", []), + ("boolean", True), + ("integer", 12), + ("null", None), + ("number", 12.0), + ("object", {}), + ("string", "foo"), + ] + ), + ) + + def test_check_schema_with_different_metaschema(self): + """ + One can create a validator class whose metaschema uses a different + dialect than itself. + """ + + NoEmptySchemasValidator = validators.create( + meta_schema={ + "$schema": validators.Draft202012Validator.META_SCHEMA["$id"], + "not": {"const": {}}, + }, + ) + NoEmptySchemasValidator.check_schema({"foo": "bar"}) + + with self.assertRaises(exceptions.SchemaError): + NoEmptySchemasValidator.check_schema({}) + + NoEmptySchemasValidator({"foo": "bar"}).validate("foo") + + def test_check_schema_with_different_metaschema_defaults_to_self(self): + """ + A validator whose metaschema doesn't declare $schema defaults to its + own validation behavior, not the latest "normal" specification. + """ + + NoEmptySchemasValidator = validators.create( + meta_schema={"fail": [{"message": "Meta schema whoops!"}]}, + validators={"fail": fail}, + ) + with self.assertRaises(exceptions.SchemaError): + NoEmptySchemasValidator.check_schema({}) + + def test_extend(self): + original = dict(self.Validator.VALIDATORS) + new = object() + + Extended = validators.extend( + self.Validator, + validators={"new": new}, + ) + self.assertEqual( + ( + Extended.VALIDATORS, + Extended.META_SCHEMA, + Extended.TYPE_CHECKER, + self.Validator.VALIDATORS, + ), ( + dict(original, new=new), + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + original, + ), + ) + + def test_extend_idof(self): + """ + Extending a validator preserves its notion of schema IDs. + """ + def id_of(schema): + return schema.get("__test__", self.Validator.ID_OF(schema)) + correct_id = "the://correct/id/" + meta_schema = { + "$id": "the://wrong/id/", + "__test__": correct_id, + } + Original = validators.create( + meta_schema=meta_schema, + validators=self.validators, + type_checker=self.type_checker, + id_of=id_of, + ) + self.assertEqual(Original.ID_OF(Original.META_SCHEMA), correct_id) + + Derived = validators.extend(Original) + self.assertEqual(Derived.ID_OF(Derived.META_SCHEMA), correct_id) + + +class TestValidationErrorMessages(TestCase): + def message_for(self, instance, schema, *args, **kwargs): + cls = kwargs.pop("cls", validators._LATEST_VERSION) + cls.check_schema(schema) + validator = cls(schema, *args, **kwargs) + errors = list(validator.iter_errors(instance)) + self.assertTrue(errors, msg=f"No errors were raised for {instance!r}") + self.assertEqual( + len(errors), + 1, + msg=f"Expected exactly one error, found {errors!r}", + ) + return errors[0].message + + def test_single_type_failure(self): + message = self.message_for(instance=1, schema={"type": "string"}) + self.assertEqual(message, "1 is not of type 'string'") + + def test_single_type_list_failure(self): + message = self.message_for(instance=1, schema={"type": ["string"]}) + self.assertEqual(message, "1 is not of type 'string'") + + def test_multiple_type_failure(self): + types = "string", "object" + message = self.message_for(instance=1, schema={"type": list(types)}) + self.assertEqual(message, "1 is not of type 'string', 'object'") + + def test_object_with_named_type_failure(self): + schema = {"type": [{"name": "Foo", "minimum": 3}]} + message = self.message_for( + instance=1, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "1 is not of type 'Foo'") + + def test_minimum(self): + message = self.message_for(instance=1, schema={"minimum": 2}) + self.assertEqual(message, "1 is less than the minimum of 2") + + def test_maximum(self): + message = self.message_for(instance=1, schema={"maximum": 0}) + self.assertEqual(message, "1 is greater than the maximum of 0") + + def test_dependencies_single_element(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: on}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_object_without_title_type_failure_draft3(self): + type = {"type": [{"minimum": 3}]} + message = self.message_for( + instance=1, + schema={"type": [type]}, + cls=validators.Draft3Validator, + ) + self.assertEqual( + message, + "1 is not of type {'type': [{'minimum': 3}]}", + ) + + def test_dependencies_list_draft3(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_dependencies_list_draft7(self): + depend, on = "bar", "foo" + schema = {"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft7Validator, + ) + self.assertEqual(message, "'foo' is a dependency of 'bar'") + + def test_additionalItems_single_failure(self): + message = self.message_for( + instance=[2], + schema={"items": [], "additionalItems": False}, + cls=validators.Draft3Validator, + ) + self.assertIn("(2 was unexpected)", message) + + def test_additionalItems_multiple_failures(self): + message = self.message_for( + instance=[1, 2, 3], + schema={"items": [], "additionalItems": False}, + cls=validators.Draft3Validator, + ) + self.assertIn("(1, 2, 3 were unexpected)", message) + + def test_additionalProperties_single_failure(self): + additional = "foo" + schema = {"additionalProperties": False} + message = self.message_for(instance={additional: 2}, schema=schema) + self.assertIn("('foo' was unexpected)", message) + + def test_additionalProperties_multiple_failures(self): + schema = {"additionalProperties": False} + message = self.message_for( + instance=dict.fromkeys(["foo", "bar"]), + schema=schema, + ) + + self.assertIn(repr("foo"), message) + self.assertIn(repr("bar"), message) + self.assertIn("were unexpected)", message) + + def test_const(self): + schema = {"const": 12} + message = self.message_for( + instance={"foo": "bar"}, + schema=schema, + ) + self.assertIn("12 was expected", message) + + def test_contains_draft_6(self): + schema = {"contains": {"const": 12}} + message = self.message_for( + instance=[2, {}, []], + schema=schema, + cls=validators.Draft6Validator, + ) + self.assertEqual( + message, + "None of [2, {}, []] are valid under the given schema", + ) + + def test_invalid_format_default_message(self): + checker = FormatChecker(formats=()) + checker.checks("thing")(lambda value: False) + + schema = {"format": "thing"} + message = self.message_for( + instance="bla", + schema=schema, + format_checker=checker, + ) + + self.assertIn(repr("bla"), message) + self.assertIn(repr("thing"), message) + self.assertIn("is not a", message) + + def test_additionalProperties_false_patternProperties(self): + schema = {"type": "object", + "additionalProperties": False, + "patternProperties": { + "^abc$": {"type": "string"}, + "^def$": {"type": "string"}, + }} + message = self.message_for( + instance={"zebra": 123}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{} does not match any of the regexes: {}, {}".format( + repr("zebra"), repr("^abc$"), repr("^def$"), + ), + ) + message = self.message_for( + instance={"zebra": 123, "fish": 456}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{}, {} do not match any of the regexes: {}, {}".format( + repr("fish"), repr("zebra"), repr("^abc$"), repr("^def$"), + ), + ) + + def test_False_schema(self): + message = self.message_for( + instance="something", + schema=False, + ) + self.assertEqual(message, "False schema does not allow 'something'") + + def test_multipleOf(self): + message = self.message_for( + instance=3, + schema={"multipleOf": 2}, + ) + self.assertEqual(message, "3 is not a multiple of 2") + + def test_minItems(self): + message = self.message_for(instance=[], schema={"minItems": 2}) + self.assertEqual(message, "[] is too short") + + def test_maxItems(self): + message = self.message_for(instance=[1, 2, 3], schema={"maxItems": 2}) + self.assertEqual(message, "[1, 2, 3] is too long") + + def test_prefixItems_with_items(self): + message = self.message_for( + instance=[1, 2, "foo", 5], + schema={"items": False, "prefixItems": [{}, {}]}, + ) + self.assertEqual(message, "Expected at most 2 items, but found 4") + + def test_minLength(self): + message = self.message_for( + instance="", + schema={"minLength": 2}, + ) + self.assertEqual(message, "'' is too short") + + def test_maxLength(self): + message = self.message_for( + instance="abc", + schema={"maxLength": 2}, + ) + self.assertEqual(message, "'abc' is too long") + + def test_pattern(self): + message = self.message_for( + instance="bbb", + schema={"pattern": "^a*$"}, + ) + self.assertEqual(message, "'bbb' does not match '^a*$'") + + def test_does_not_contain(self): + message = self.message_for( + instance=[], + schema={"contains": {"type": "string"}}, + ) + self.assertEqual( + message, + "[] does not contain items matching the given schema", + ) + + def test_contains_too_few(self): + message = self.message_for( + instance=["foo", 1], + schema={"contains": {"type": "string"}, "minContains": 2}, + ) + self.assertEqual( + message, + "Too few items match the given schema " + "(expected at least 2 but only 1 matched)", + ) + + def test_contains_too_few_both_constrained(self): + message = self.message_for( + instance=["foo", 1], + schema={ + "contains": {"type": "string"}, + "minContains": 2, + "maxContains": 4, + }, + ) + self.assertEqual( + message, + "Too few items match the given schema (expected at least 2 but " + "only 1 matched)", + ) + + def test_contains_too_many(self): + message = self.message_for( + instance=["foo", "bar", "baz"], + schema={"contains": {"type": "string"}, "maxContains": 2}, + ) + self.assertEqual( + message, + "Too many items match the given schema (expected at most 2)", + ) + + def test_contains_too_many_both_constrained(self): + message = self.message_for( + instance=["foo"] * 5, + schema={ + "contains": {"type": "string"}, + "minContains": 2, + "maxContains": 4, + }, + ) + self.assertEqual( + message, + "Too many items match the given schema (expected at most 4)", + ) + + def test_exclusiveMinimum(self): + message = self.message_for( + instance=3, + schema={"exclusiveMinimum": 5}, + ) + self.assertEqual( + message, + "3 is less than or equal to the minimum of 5", + ) + + def test_exclusiveMaximum(self): + message = self.message_for(instance=3, schema={"exclusiveMaximum": 2}) + self.assertEqual( + message, + "3 is greater than or equal to the maximum of 2", + ) + + def test_required(self): + message = self.message_for(instance={}, schema={"required": ["foo"]}) + self.assertEqual(message, "'foo' is a required property") + + def test_dependentRequired(self): + message = self.message_for( + instance={"foo": {}}, + schema={"dependentRequired": {"foo": ["bar"]}}, + ) + self.assertEqual(message, "'bar' is a dependency of 'foo'") + + def test_minProperties(self): + message = self.message_for(instance={}, schema={"minProperties": 2}) + self.assertEqual(message, "{} does not have enough properties") + + def test_maxProperties(self): + message = self.message_for( + instance={"a": {}, "b": {}, "c": {}}, + schema={"maxProperties": 2}, + ) + self.assertEqual( + message, + "{'a': {}, 'b': {}, 'c': {}} has too many properties", + ) + + def test_oneOf_matches_none(self): + message = self.message_for(instance={}, schema={"oneOf": [False]}) + self.assertEqual( + message, + "{} is not valid under any of the given schemas", + ) + + def test_oneOf_matches_too_many(self): + message = self.message_for(instance={}, schema={"oneOf": [True, True]}) + self.assertEqual(message, "{} is valid under each of True, True") + + def test_unevaluated_items(self): + schema = {"type": "array", "unevaluatedItems": False} + message = self.message_for(instance=["foo", "bar"], schema=schema) + self.assertIn( + message, + "Unevaluated items are not allowed ('bar', 'foo' were unexpected)", + ) + + def test_unevaluated_items_on_invalid_type(self): + schema = {"type": "array", "unevaluatedItems": False} + message = self.message_for(instance="foo", schema=schema) + self.assertEqual(message, "'foo' is not of type 'array'") + + def test_unevaluated_properties_invalid_against_subschema(self): + schema = { + "properties": {"foo": {"type": "string"}}, + "unevaluatedProperties": {"const": 12}, + } + message = self.message_for( + instance={ + "foo": "foo", + "bar": "bar", + "baz": 12, + }, + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated properties are not valid under the given schema " + "('bar' was unevaluated and invalid)", + ) + + def test_unevaluated_properties_disallowed(self): + schema = {"type": "object", "unevaluatedProperties": False} + message = self.message_for( + instance={ + "foo": "foo", + "bar": "bar", + }, + schema=schema, + ) + self.assertEqual( + message, + "Unevaluated properties are not allowed " + "('bar', 'foo' were unexpected)", + ) + + def test_unevaluated_properties_on_invalid_type(self): + schema = {"type": "object", "unevaluatedProperties": False} + message = self.message_for(instance="foo", schema=schema) + self.assertEqual(message, "'foo' is not of type 'object'") + + +class TestValidationErrorDetails(TestCase): + # TODO: These really need unit tests for each individual keyword, rather + # than just these higher level tests. + def test_anyOf(self): + instance = 5 + schema = { + "anyOf": [ + {"minimum": 20}, + {"type": "string"}, + ], + } + + validator = validators.Draft4Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "anyOf") + self.assertEqual(e.validator_value, schema["anyOf"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.json_path, "$") + + self.assertEqual(e.schema_path, deque(["anyOf"])) + self.assertEqual(e.relative_schema_path, deque(["anyOf"])) + self.assertEqual(e.absolute_schema_path, deque(["anyOf"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "minimum") + self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["anyOf"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.json_path, "$") + + self.assertEqual(e1.schema_path, deque([0, "minimum"])) + self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) + self.assertEqual( + e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), + ) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "type") + self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) + self.assertEqual(e2.instance, instance) + self.assertEqual(e2.schema, schema["anyOf"][1]) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque([])) + self.assertEqual(e2.relative_path, deque([])) + self.assertEqual(e2.absolute_path, deque([])) + self.assertEqual(e2.json_path, "$") + + self.assertEqual(e2.schema_path, deque([1, "type"])) + self.assertEqual(e2.relative_schema_path, deque([1, "type"])) + self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"])) + + self.assertEqual(len(e2.context), 0) + + def test_type(self): + instance = {"foo": 1} + schema = { + "type": [ + {"type": "integer"}, + { + "type": "object", + "properties": {"foo": {"enum": [2]}}, + }, + ], + } + + validator = validators.Draft3Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "type") + self.assertEqual(e.validator_value, schema["type"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + self.assertEqual(e.json_path, "$") + + self.assertEqual(e.schema_path, deque(["type"])) + self.assertEqual(e.relative_schema_path, deque(["type"])) + self.assertEqual(e.absolute_schema_path, deque(["type"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e1.validator_value, schema["type"][0]["type"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["type"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.json_path, "$") + + self.assertEqual(e1.schema_path, deque([0, "type"])) + self.assertEqual(e1.relative_schema_path, deque([0, "type"])) + self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"])) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "enum") + self.assertEqual(e2.validator_value, [2]) + self.assertEqual(e2.instance, 1) + self.assertEqual(e2.schema, {"enum": [2]}) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque(["foo"])) + self.assertEqual(e2.relative_path, deque(["foo"])) + self.assertEqual(e2.absolute_path, deque(["foo"])) + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual( + e2.schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.absolute_schema_path, + deque(["type", 1, "properties", "foo", "enum"]), + ) + + self.assertFalse(e2.context) + + def test_single_nesting(self): + instance = {"foo": 2, "bar": [1], "baz": 15, "quux": "spam"} + schema = { + "properties": { + "foo": {"type": "string"}, + "bar": {"minItems": 2}, + "baz": {"maximum": 10, "enum": [2, 4, 6, 8]}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["baz"])) + self.assertEqual(e3.path, deque(["baz"])) + self.assertEqual(e4.path, deque(["foo"])) + + self.assertEqual(e1.relative_path, deque(["bar"])) + self.assertEqual(e2.relative_path, deque(["baz"])) + self.assertEqual(e3.relative_path, deque(["baz"])) + self.assertEqual(e4.relative_path, deque(["foo"])) + + self.assertEqual(e1.absolute_path, deque(["bar"])) + self.assertEqual(e2.absolute_path, deque(["baz"])) + self.assertEqual(e3.absolute_path, deque(["baz"])) + self.assertEqual(e4.absolute_path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.baz") + self.assertEqual(e3.json_path, "$.baz") + self.assertEqual(e4.json_path, "$.foo") + + self.assertEqual(e1.validator, "minItems") + self.assertEqual(e2.validator, "enum") + self.assertEqual(e3.validator, "maximum") + self.assertEqual(e4.validator, "type") + + def test_multiple_nesting(self): + instance = [1, {"foo": 2, "bar": {"baz": [1]}}, "quux"] + schema = { + "type": "string", + "items": { + "type": ["string", "object"], + "properties": { + "foo": {"enum": [1, 3]}, + "bar": { + "type": "array", + "properties": { + "bar": {"required": True}, + "baz": {"minItems": 2}, + }, + }, + }, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4, e5, e6 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e2.path, deque([0])) + self.assertEqual(e3.path, deque([1, "bar"])) + self.assertEqual(e4.path, deque([1, "bar", "bar"])) + self.assertEqual(e5.path, deque([1, "bar", "baz"])) + self.assertEqual(e6.path, deque([1, "foo"])) + + self.assertEqual(e1.json_path, "$") + self.assertEqual(e2.json_path, "$[0]") + self.assertEqual(e3.json_path, "$[1].bar") + self.assertEqual(e4.json_path, "$[1].bar.bar") + self.assertEqual(e5.json_path, "$[1].bar.baz") + self.assertEqual(e6.json_path, "$[1].foo") + + self.assertEqual(e1.schema_path, deque(["type"])) + self.assertEqual(e2.schema_path, deque(["items", "type"])) + self.assertEqual( + list(e3.schema_path), ["items", "properties", "bar", "type"], + ) + self.assertEqual( + list(e4.schema_path), + ["items", "properties", "bar", "properties", "bar", "required"], + ) + self.assertEqual( + list(e5.schema_path), + ["items", "properties", "bar", "properties", "baz", "minItems"], + ) + self.assertEqual( + list(e6.schema_path), ["items", "properties", "foo", "enum"], + ) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "type") + self.assertEqual(e3.validator, "type") + self.assertEqual(e4.validator, "required") + self.assertEqual(e5.validator, "minItems") + self.assertEqual(e6.validator, "enum") + + def test_recursive(self): + schema = { + "definitions": { + "node": { + "anyOf": [{ + "type": "object", + "required": ["name", "children"], + "properties": { + "name": { + "type": "string", + }, + "children": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/node", + }, + }, + }, + }, + }], + }, + }, + "type": "object", + "required": ["root"], + "properties": {"root": {"$ref": "#/definitions/node"}}, + } + + instance = { + "root": { + "name": "root", + "children": { + "a": { + "name": "a", + "children": { + "ab": { + "name": "ab", + # missing "children" + }, + }, + }, + }, + }, + } + validator = validators.Draft4Validator(schema) + + e, = validator.iter_errors(instance) + self.assertEqual(e.absolute_path, deque(["root"])) + self.assertEqual( + e.absolute_schema_path, deque(["properties", "root", "anyOf"]), + ) + self.assertEqual(e.json_path, "$.root") + + e1, = e.context + self.assertEqual(e1.absolute_path, deque(["root", "children", "a"])) + self.assertEqual( + e1.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + self.assertEqual(e1.json_path, "$.root.children.a") + + e2, = e1.context + self.assertEqual( + e2.absolute_path, deque( + ["root", "children", "a", "children", "ab"], + ), + ) + self.assertEqual( + e2.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + self.assertEqual(e2.json_path, "$.root.children.a.children.ab") + + def test_additionalProperties(self): + instance = {"bar": "bar", "foo": 2} + schema = {"additionalProperties": {"type": "integer", "minimum": 5}} + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_patternProperties(self): + instance = {"bar": 1, "foo": 2} + schema = { + "patternProperties": { + "bar": {"type": "string"}, + "foo": {"minimum": 5}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.json_path, "$.bar") + self.assertEqual(e2.json_path, "$.foo") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems(self): + instance = ["foo", 1] + schema = { + "items": [], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([0])) + self.assertEqual(e2.path, deque([1])) + + self.assertEqual(e1.json_path, "$[0]") + self.assertEqual(e2.json_path, "$[1]") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems_with_items(self): + instance = ["foo", "bar", 1] + schema = { + "items": [{}], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([1])) + self.assertEqual(e2.path, deque([2])) + + self.assertEqual(e1.json_path, "$[1]") + self.assertEqual(e2.json_path, "$[2]") + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_propertyNames(self): + instance = {"foo": 12} + schema = {"propertyNames": {"not": {"const": "foo"}}} + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(instance) + + self.assertEqual(error.validator, "not") + self.assertEqual( + error.message, + "'foo' should not be valid under {'const': 'foo'}", + ) + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) + + def test_if_then(self): + schema = { + "if": {"const": 12}, + "then": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(12) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["then", "const"])) + + def test_if_else(self): + schema = { + "if": {"const": 12}, + "else": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(15) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.json_path, "$") + self.assertEqual(error.schema_path, deque(["else", "const"])) + + def test_boolean_schema_False(self): + validator = validators.Draft7Validator(False) + error, = validator.iter_errors(12) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "False schema does not allow 12", + None, + None, + 12, + False, + deque([]), + "$", + ), + ) + + def test_ref(self): + ref, schema = "someRef", {"additionalProperties": {"type": "integer"}} + validator = validators.Draft7Validator( + {"$ref": ref}, + resolver=validators._RefResolver("", {}, store={ref: schema}), + ) + error, = validator.iter_errors({"foo": "notAnInteger"}) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "'notAnInteger' is not of type 'integer'", + "type", + "integer", + "notAnInteger", + deque(["foo"]), + {"type": "integer"}, + deque(["additionalProperties", "type"]), + "$.foo", + ), + ) + + def test_prefixItems(self): + schema = {"prefixItems": [{"type": "string"}, {}, {}, {"maximum": 3}]} + validator = validators.Draft202012Validator(schema) + type_error, min_error = validator.iter_errors([1, 2, "foo", 5]) + self.assertEqual( + ( + type_error.message, + type_error.validator, + type_error.validator_value, + type_error.instance, + type_error.absolute_path, + type_error.schema, + type_error.schema_path, + type_error.json_path, + ), + ( + "1 is not of type 'string'", + "type", + "string", + 1, + deque([0]), + {"type": "string"}, + deque(["prefixItems", 0, "type"]), + "$[0]", + ), + ) + self.assertEqual( + ( + min_error.message, + min_error.validator, + min_error.validator_value, + min_error.instance, + min_error.absolute_path, + min_error.schema, + min_error.schema_path, + min_error.json_path, + ), + ( + "5 is greater than the maximum of 3", + "maximum", + 3, + 5, + deque([3]), + {"maximum": 3}, + deque(["prefixItems", 3, "maximum"]), + "$[3]", + ), + ) + + def test_prefixItems_with_items(self): + schema = { + "items": {"type": "string"}, + "prefixItems": [{}], + } + validator = validators.Draft202012Validator(schema) + e1, e2 = validator.iter_errors(["foo", 2, "bar", 4, "baz"]) + self.assertEqual( + ( + e1.message, + e1.validator, + e1.validator_value, + e1.instance, + e1.absolute_path, + e1.schema, + e1.schema_path, + e1.json_path, + ), + ( + "2 is not of type 'string'", + "type", + "string", + 2, + deque([1]), + {"type": "string"}, + deque(["items", "type"]), + "$[1]", + ), + ) + self.assertEqual( + ( + e2.message, + e2.validator, + e2.validator_value, + e2.instance, + e2.absolute_path, + e2.schema, + e2.schema_path, + e2.json_path, + ), + ( + "4 is not of type 'string'", + "type", + "string", + 4, + deque([3]), + {"type": "string"}, + deque(["items", "type"]), + "$[3]", + ), + ) + + def test_contains_too_many(self): + """ + `contains` + `maxContains` produces only one error, even if there are + many more incorrectly matching elements. + """ + schema = {"contains": {"type": "string"}, "maxContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors(["foo", 2, "bar", 4, "baz", "quux"]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "Too many items match the given schema (expected at most 2)", + "maxContains", + 2, + ["foo", 2, "bar", 4, "baz", "quux"], + deque([]), + {"contains": {"type": "string"}, "maxContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_contains_too_few(self): + schema = {"contains": {"type": "string"}, "minContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors(["foo", 2, 4]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + ( + "Too few items match the given schema " + "(expected at least 2 but only 1 matched)" + ), + "minContains", + 2, + ["foo", 2, 4], + deque([]), + {"contains": {"type": "string"}, "minContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_contains_none(self): + schema = {"contains": {"type": "string"}, "minContains": 2} + validator = validators.Draft202012Validator(schema) + error, = validator.iter_errors([2, 4]) + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + error.json_path, + ), + ( + "[2, 4] does not contain items matching the given schema", + "contains", + {"type": "string"}, + [2, 4], + deque([]), + {"contains": {"type": "string"}, "minContains": 2}, + deque(["contains"]), + "$", + ), + ) + + def test_ref_sibling(self): + schema = { + "$defs": {"foo": {"required": ["bar"]}}, + "properties": { + "aprop": { + "$ref": "#/$defs/foo", + "required": ["baz"], + }, + }, + } + + validator = validators.Draft202012Validator(schema) + e1, e2 = validator.iter_errors({"aprop": {}}) + self.assertEqual( + ( + e1.message, + e1.validator, + e1.validator_value, + e1.instance, + e1.absolute_path, + e1.schema, + e1.schema_path, + e1.relative_schema_path, + e1.json_path, + ), + ( + "'bar' is a required property", + "required", + ["bar"], + {}, + deque(["aprop"]), + {"required": ["bar"]}, + deque(["properties", "aprop", "required"]), + deque(["properties", "aprop", "required"]), + "$.aprop", + ), + ) + self.assertEqual( + ( + e2.message, + e2.validator, + e2.validator_value, + e2.instance, + e2.absolute_path, + e2.schema, + e2.schema_path, + e2.relative_schema_path, + e2.json_path, + ), + ( + "'baz' is a required property", + "required", + ["baz"], + {}, + deque(["aprop"]), + {"$ref": "#/$defs/foo", "required": ["baz"]}, + deque(["properties", "aprop", "required"]), + deque(["properties", "aprop", "required"]), + "$.aprop", + ), + ) + + +class MetaSchemaTestsMixin: + # TODO: These all belong upstream + def test_invalid_properties(self): + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"properties": 12}) + + def test_minItems_invalid_string(self): + with self.assertRaises(exceptions.SchemaError): + # needs to be an integer + self.Validator.check_schema({"minItems": "1"}) + + def test_enum_allows_empty_arrays(self): + """ + Technically, all the spec says is they SHOULD have elements, not MUST. + + (As of Draft 6. Previous drafts do say MUST). + + See #529. + """ + if self.Validator in { + validators.Draft3Validator, + validators.Draft4Validator, + }: + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"enum": []}) + else: + self.Validator.check_schema({"enum": []}) + + def test_enum_allows_non_unique_items(self): + """ + Technically, all the spec says is they SHOULD be unique, not MUST. + + (As of Draft 6. Previous drafts do say MUST). + + See #529. + """ + if self.Validator in { + validators.Draft3Validator, + validators.Draft4Validator, + }: + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"enum": [12, 12]}) + else: + self.Validator.check_schema({"enum": [12, 12]}) + + def test_schema_with_invalid_regex(self): + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"pattern": "*notaregex"}) + + def test_schema_with_invalid_regex_with_disabled_format_validation(self): + self.Validator.check_schema( + {"pattern": "*notaregex"}, + format_checker=None, + ) + + +class ValidatorTestMixin(MetaSchemaTestsMixin): + def test_it_implements_the_validator_protocol(self): + self.assertIsInstance(self.Validator({}), protocols.Validator) + + def test_valid_instances_are_valid(self): + schema, instance = self.valid + self.assertTrue(self.Validator(schema).is_valid(instance)) + + def test_invalid_instances_are_not_valid(self): + schema, instance = self.invalid + self.assertFalse(self.Validator(schema).is_valid(instance)) + + def test_non_existent_properties_are_ignored(self): + self.Validator({object(): object()}).validate(instance=object()) + + def test_evolve(self): + schema, format_checker = {"type": "integer"}, FormatChecker() + original = self.Validator( + schema, + format_checker=format_checker, + ) + new = original.evolve( + schema={"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + ) + + expected = self.Validator( + {"type": "string"}, + format_checker=self.Validator.FORMAT_CHECKER, + _resolver=new._resolver, + ) + + self.assertEqual(new, expected) + self.assertNotEqual(new, original) + + def test_evolve_with_subclass(self): + """ + Subclassing validators isn't supported public API, but some users have + done it, because we don't actually error entirely when it's done :/ + + We need to deprecate doing so first to help as many of these users + ensure they can move to supported APIs, but this test ensures that in + the interim, we haven't broken those users. + """ + + with self.assertWarns(DeprecationWarning): + @attr.s + class OhNo(self.Validator): + foo = attr.ib(factory=lambda: [1, 2, 3]) + _bar = attr.ib(default=37) + + validator = OhNo({}, bar=12) + self.assertEqual(validator.foo, [1, 2, 3]) + + new = validator.evolve(schema={"type": "integer"}) + self.assertEqual(new.foo, [1, 2, 3]) + self.assertEqual(new._bar, 12) + + def test_is_type_is_true_for_valid_type(self): + self.assertTrue(self.Validator({}).is_type("foo", "string")) + + def test_is_type_is_false_for_invalid_type(self): + self.assertFalse(self.Validator({}).is_type("foo", "array")) + + def test_is_type_evades_bool_inheriting_from_int(self): + self.assertFalse(self.Validator({}).is_type(True, "integer")) + self.assertFalse(self.Validator({}).is_type(True, "number")) + + def test_it_can_validate_with_decimals(self): + schema = {"items": {"type": "number"}} + Validator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "number", + lambda checker, thing: isinstance( + thing, (int, float, Decimal), + ) and not isinstance(thing, bool), + ), + ) + + validator = Validator(schema) + validator.validate([1, 1.1, Decimal(1) / Decimal(8)]) + + invalid = ["foo", {}, [], True, None] + self.assertEqual( + [error.instance for error in validator.iter_errors(invalid)], + invalid, + ) + + def test_it_returns_true_for_formats_it_does_not_know_about(self): + validator = self.Validator( + {"format": "carrot"}, format_checker=FormatChecker(), + ) + validator.validate("bugs") + + def test_it_does_not_validate_formats_by_default(self): + validator = self.Validator({}) + self.assertIsNone(validator.format_checker) + + def test_it_validates_formats_if_a_checker_is_provided(self): + checker = FormatChecker() + bad = ValueError("Bad!") + + @checker.checks("foo", raises=ValueError) + def check(value): + if value == "good": + return True + elif value == "bad": + raise bad + else: # pragma: no cover + self.fail(f"What is {value}? [Baby Don't Hurt Me]") + + validator = self.Validator( + {"format": "foo"}, format_checker=checker, + ) + + validator.validate("good") + with self.assertRaises(exceptions.ValidationError) as cm: + validator.validate("bad") + + # Make sure original cause is attached + self.assertIs(cm.exception.cause, bad) + + def test_non_string_custom_type(self): + non_string_type = object() + schema = {"type": [non_string_type]} + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + non_string_type, + lambda checker, thing: isinstance(thing, int), + ), + ) + Crazy(schema).validate(15) + + def test_it_properly_formats_tuples_in_errors(self): + """ + A tuple instance properly formats validation errors for uniqueItems. + + See #224 + """ + TupleValidator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "array", + lambda checker, thing: isinstance(thing, tuple), + ), + ) + with self.assertRaises(exceptions.ValidationError) as e: + TupleValidator({"uniqueItems": True}).validate((1, 1)) + self.assertIn("(1, 1) has non-unique elements", str(e.exception)) + + def test_check_redefined_sequence(self): + """ + Allow array to validate against another defined sequence type + """ + schema = {"type": "array", "uniqueItems": True} + MyMapping = namedtuple("MyMapping", "a, b") + Validator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine_many( + { + "array": lambda checker, thing: isinstance( + thing, (list, deque), + ), + "object": lambda checker, thing: isinstance( + thing, (dict, MyMapping), + ), + }, + ), + ) + validator = Validator(schema) + + valid_instances = [ + deque(["a", None, "1", "", True]), + deque([[False], [0]]), + [deque([False]), deque([0])], + [[deque([False])], [deque([0])]], + [[[[[deque([False])]]]], [[[[deque([0])]]]]], + [deque([deque([False])]), deque([deque([0])])], + [MyMapping("a", 0), MyMapping("a", False)], + [ + MyMapping("a", [deque([0])]), + MyMapping("a", [deque([False])]), + ], + [ + MyMapping("a", [MyMapping("a", deque([0]))]), + MyMapping("a", [MyMapping("a", deque([False]))]), + ], + [deque(deque(deque([False]))), deque(deque(deque([0])))], + ] + + for instance in valid_instances: + validator.validate(instance) + + invalid_instances = [ + deque(["a", "b", "a"]), + deque([[False], [False]]), + [deque([False]), deque([False])], + [[deque([False])], [deque([False])]], + [[[[[deque([False])]]]], [[[[deque([False])]]]]], + [deque([deque([False])]), deque([deque([False])])], + [MyMapping("a", False), MyMapping("a", False)], + [ + MyMapping("a", [deque([False])]), + MyMapping("a", [deque([False])]), + ], + [ + MyMapping("a", [MyMapping("a", deque([False]))]), + MyMapping("a", [MyMapping("a", deque([False]))]), + ], + [deque(deque(deque([False]))), deque(deque(deque([False])))], + ] + + for instance in invalid_instances: + with self.assertRaises(exceptions.ValidationError): + validator.validate(instance) + + def test_it_creates_a_ref_resolver_if_not_provided(self): + with self.assertWarns(DeprecationWarning): + resolver = self.Validator({}).resolver + self.assertIsInstance(resolver, validators._RefResolver) + + def test_it_upconverts_from_deprecated_RefResolvers(self): + ref, schema = "someCoolRef", {"type": "integer"} + resolver = validators._RefResolver("", {}, store={ref: schema}) + validator = self.Validator({"$ref": ref}, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + def test_it_upconverts_from_yet_older_deprecated_legacy_RefResolvers(self): + """ + Legacy RefResolvers support only the context manager form of + resolution. + """ + + class LegacyRefResolver: + @contextmanager + def resolving(this, ref): + self.assertEqual(ref, "the ref") + yield {"type": "integer"} + + resolver = LegacyRefResolver() + schema = {"$ref": "the ref"} + + with self.assertRaises(exceptions.ValidationError): + self.Validator(schema, resolver=resolver).validate(None) + + +class AntiDraft6LeakMixin: + """ + Make sure functionality from draft 6 doesn't leak backwards in time. + """ + + def test_True_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(True) + self.assertIn("True is not of type", str(e.exception)) + + def test_False_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(False) + self.assertIn("False is not of type", str(e.exception)) + + def test_True_is_not_a_schema_even_if_you_forget_to_check(self): + with self.assertRaises(Exception) as e: + self.Validator(True).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + def test_False_is_not_a_schema_even_if_you_forget_to_check(self): + with self.assertRaises(Exception) as e: + self.Validator(False).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + +class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft3Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + def test_any_type_is_valid_for_type_any(self): + validator = self.Validator({"type": "any"}) + validator.validate(object()) + + def test_any_type_is_redefinable(self): + """ + Sigh, because why not. + """ + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "any", lambda checker, thing: isinstance(thing, int), + ), + ) + validator = Crazy({"type": "any"}) + validator.validate(12) + with self.assertRaises(exceptions.ValidationError): + validator.validate("foo") + + def test_is_type_is_true_for_any_type(self): + self.assertTrue(self.Validator({"type": "any"}).is_valid(object())) + + def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): + self.assertTrue(self.Validator({}).is_type(True, "boolean")) + self.assertTrue(self.Validator({"type": "any"}).is_valid(True)) + + +class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft4Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft6Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft6Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft7Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft7Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft201909Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft201909Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestDraft202012Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft202012Validator + valid: tuple[dict, dict] = ({}, {}) + invalid = {"type": "integer"}, "foo" + + +class TestLatestValidator(TestCase): + """ + These really apply to multiple versions but are easiest to test on one. + """ + + def test_ref_resolvers_may_have_boolean_schemas_stored(self): + ref = "someCoolRef" + schema = {"$ref": ref} + resolver = validators._RefResolver("", {}, store={ref: False}) + validator = validators._LATEST_VERSION(schema, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + +class TestValidatorFor(TestCase): + def test_draft_3(self): + schema = {"$schema": "http://json-schema.org/draft-03/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-03/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + def test_draft_4(self): + schema = {"$schema": "http://json-schema.org/draft-04/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-04/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + def test_draft_6(self): + schema = {"$schema": "http://json-schema.org/draft-06/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-06/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + def test_draft_7(self): + schema = {"$schema": "http://json-schema.org/draft-07/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-07/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + def test_draft_201909(self): + schema = {"$schema": "https://json-schema.org/draft/2019-09/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft201909Validator, + ) + + schema = {"$schema": "https://json-schema.org/draft/2019-09/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft201909Validator, + ) + + def test_draft_202012(self): + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + + schema = {"$schema": "https://json-schema.org/draft/2020-12/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft202012Validator, + ) + + def test_True(self): + self.assertIs( + validators.validator_for(True), + validators._LATEST_VERSION, + ) + + def test_False(self): + self.assertIs( + validators.validator_for(False), + validators._LATEST_VERSION, + ) + + def test_custom_validator(self): + Validator = validators.create( + meta_schema={"id": "meta schema id"}, + version="12", + id_of=lambda s: s.get("id", ""), + ) + schema = {"$schema": "meta schema id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_custom_validator_draft6(self): + Validator = validators.create( + meta_schema={"$id": "meta schema $id"}, + version="13", + ) + schema = {"$schema": "meta schema $id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_validator_for_jsonschema_default(self): + self.assertIs(validators.validator_for({}), validators._LATEST_VERSION) + + def test_validator_for_custom_default(self): + self.assertIs(validators.validator_for({}, default=None), None) + + def test_warns_if_meta_schema_specified_was_not_found(self): + with self.assertWarns(DeprecationWarning) as cm: + validators.validator_for(schema={"$schema": "unknownSchema"}) + + self.assertEqual(cm.filename, __file__) + self.assertEqual( + str(cm.warning), + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future.", + ) + + def test_does_not_warn_if_meta_schema_is_unspecified(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + validators.validator_for(schema={}, default={}) + self.assertFalse(w) + + def test_validator_for_custom_default_with_schema(self): + schema, default = {"$schema": "mailto:foo@example.com"}, object() + self.assertIs(validators.validator_for(schema, default), default) + + +class TestValidate(TestCase): + def assertUses(self, schema, Validator): + result = [] + with mock.patch.object(Validator, "check_schema", result.append): + validators.validate({}, schema) + self.assertEqual(result, [schema]) + + def test_draft3_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema#"}, + Validator=validators.Draft3Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema"}, + Validator=validators.Draft3Validator, + ) + + def test_draft4_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema#"}, + Validator=validators.Draft4Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema"}, + Validator=validators.Draft4Validator, + ) + + def test_draft6_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema#"}, + Validator=validators.Draft6Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema"}, + Validator=validators.Draft6Validator, + ) + + def test_draft7_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema#"}, + Validator=validators.Draft7Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema"}, + Validator=validators.Draft7Validator, + ) + + def test_draft202012_validator_is_chosen(self): + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema#", + }, + Validator=validators.Draft202012Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={ + "$schema": "https://json-schema.org/draft/2020-12/schema", + }, + Validator=validators.Draft202012Validator, + ) + + def test_draft202012_validator_is_the_default(self): + self.assertUses(schema={}, Validator=validators.Draft202012Validator) + + def test_validation_error_message(self): + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, {"type": "string"}) + self.assertRegex( + str(e.exception), + "(?s)Failed validating '.*' in schema.*On instance", + ) + + def test_schema_error_message(self): + with self.assertRaises(exceptions.SchemaError) as e: + validators.validate(12, {"type": 12}) + self.assertRegex( + str(e.exception), + "(?s)Failed validating '.*' in metaschema.*On schema", + ) + + def test_it_uses_best_match(self): + schema = { + "oneOf": [ + {"type": "number", "minimum": 20}, + {"type": "array"}, + ], + } + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, schema) + self.assertIn("12 is less than the minimum of 20", str(e.exception)) + + +class TestThreading(TestCase): + """ + Threading-related functionality tests. + + jsonschema doesn't promise thread safety, and its validation behavior + across multiple threads may change at any time, but that means it isn't + safe to share *validators* across threads, not that anytime one has + multiple threads that jsonschema won't work (it certainly is intended to). + + These tests ensure that this minimal level of functionality continues to + work. + """ + + def test_validation_across_a_second_thread(self): + failed = [] + + def validate(): + try: + validators.validate(instance=37, schema=True) + except: # noqa: E722, pragma: no cover + failed.append(sys.exc_info()) + + validate() # just verify it succeeds + + from threading import Thread + thread = Thread(target=validate) + thread.start() + thread.join() + self.assertEqual((thread.is_alive(), failed), (False, [])) + + +class TestRefResolver(TestCase): + + base_uri = "" + stored_uri = "foo://stored" + stored_schema = {"stored": "schema"} + + def setUp(self): + self.referrer = {} + self.store = {self.stored_uri: self.stored_schema} + self.resolver = validators._RefResolver( + self.base_uri, self.referrer, self.store, + ) + + def test_it_does_not_retrieve_schema_urls_from_the_network(self): + ref = validators.Draft3Validator.META_SCHEMA["id"] + with mock.patch.object(self.resolver, "resolve_remote") as patched: + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, validators.Draft3Validator.META_SCHEMA) + self.assertFalse(patched.called) + + def test_it_resolves_local_refs(self): + ref = "#/properties/foo" + self.referrer["properties"] = {"foo": object()} + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, self.referrer["properties"]["foo"]) + + def test_it_resolves_local_refs_with_id(self): + schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}} + resolver = validators._RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get("id", ""), + ) + with resolver.resolving("#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + with resolver.resolving("http://bar/schema#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + + def test_it_retrieves_stored_refs(self): + with self.resolver.resolving(self.stored_uri) as resolved: + self.assertIs(resolved, self.stored_schema) + + self.resolver.store["cached_ref"] = {"foo": 12} + with self.resolver.resolving("cached_ref#/foo") as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_requests(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = ReallyFakeRequests({"http://bar": schema}) + + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_urlopen(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = None + + @contextmanager + def fake_urlopen(url): + self.assertEqual(url, "http://bar") + yield BytesIO(json.dumps(schema).encode("utf8")) + + self.addCleanup(setattr, validators, "urlopen", validators.urlopen) + validators.urlopen = fake_urlopen + + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, 12) + + def test_it_retrieves_local_refs_via_urlopen(self): + with tempfile.NamedTemporaryFile(delete=False, mode="wt") as tempf: + self.addCleanup(os.remove, tempf.name) + json.dump({"foo": "bar"}, tempf) + + ref = f"file://{pathname2url(tempf.name)}#foo" + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, "bar") + + def test_it_can_construct_a_base_uri_from_a_schema(self): + schema = {"id": "foo"} + resolver = validators._RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get("id", ""), + ) + self.assertEqual(resolver.base_uri, "foo") + self.assertEqual(resolver.resolution_scope, "foo") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo#") as resolved: + self.assertEqual(resolved, schema) + + def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): + schema = {} + resolver = validators._RefResolver.from_schema(schema) + self.assertEqual(resolver.base_uri, "") + self.assertEqual(resolver.resolution_scope, "") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + + def test_custom_uri_scheme_handlers(self): + def handler(url): + self.assertEqual(url, ref) + return schema + + schema = {"foo": "bar"} + ref = "foo://bar" + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) + with resolver.resolving(ref) as resolved: + self.assertEqual(resolved, schema) + + def test_cache_remote_on(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Response must not have been cached!") + + ref = "foo://bar" + resolver = validators._RefResolver( + "", {}, cache_remote=True, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + with resolver.resolving(ref): + pass + + def test_cache_remote_off(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Handler called twice!") + + ref = "foo://bar" + resolver = validators._RefResolver( + "", {}, cache_remote=False, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + + def test_if_you_give_it_junk_you_get_a_resolution_error(self): + error = ValueError("Oh no! What's this?") + + def handler(url): + raise error + + ref = "foo://bar" + resolver = validators._RefResolver("", {}, handlers={"foo": handler}) + with self.assertRaises(exceptions._RefResolutionError) as err: + with resolver.resolving(ref): + self.fail("Shouldn't get this far!") # pragma: no cover + self.assertEqual(err.exception, exceptions._RefResolutionError(error)) + + def test_helpful_error_message_on_failed_pop_scope(self): + resolver = validators._RefResolver("", {}) + resolver.pop_scope() + with self.assertRaises(exceptions._RefResolutionError) as exc: + resolver.pop_scope() + self.assertIn("Failed to pop the scope", str(exc.exception)) + + +def sorted_errors(errors): + def key(error): + return ( + [str(e) for e in error.path], + [str(e) for e in error.schema_path], + ) + return sorted(errors, key=key) + + +@attr.s +class ReallyFakeRequests: + + _responses = attr.ib() + + def get(self, url): + response = self._responses.get(url) + if url is None: # pragma: no cover + raise ValueError("Unknown URL: " + repr(url)) + return _ReallyFakeJSONResponse(json.dumps(response)) + + +@attr.s +class _ReallyFakeJSONResponse: + + _response = attr.ib() + + def json(self): + return json.loads(self._response) diff --git a/jsonschema/validators.py b/jsonschema/validators.py new file mode 100644 index 0000000..23ea17c --- /dev/null +++ b/jsonschema/validators.py @@ -0,0 +1,1335 @@ +""" +Creation and extension of validators, with implementations for existing drafts. +""" +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable, Mapping, Sequence +from functools import lru_cache +from operator import methodcaller +from urllib.parse import unquote, urldefrag, urljoin, urlsplit +from urllib.request import urlopen +from warnings import warn +import contextlib +import json +import reprlib +import warnings + +from attrs import define, field, fields +from jsonschema_specifications import REGISTRY as SPECIFICATIONS +from referencing import Specification +from rpds import HashTrieMap +import referencing.jsonschema + +from jsonschema import ( + _format, + _legacy_validators, + _types, + _typing, + _utils, + _validators, + exceptions, +) +from jsonschema.protocols import Validator + +_UNSET = _utils.Unset() + +_VALIDATORS: dict[str, Validator] = {} +_META_SCHEMAS = _utils.URIDict() + + +def __getattr__(name): + if name == "ErrorTree": + warnings.warn( + "Importing ErrorTree from jsonschema.validators is deprecated. " + "Instead import it from jsonschema.exceptions.", + DeprecationWarning, + stacklevel=2, + ) + from jsonschema.exceptions import ErrorTree + return ErrorTree + elif name == "validators": + warnings.warn( + "Accessing jsonschema.validators.validators is deprecated. " + "Use jsonschema.validators.validator_for with a given schema.", + DeprecationWarning, + stacklevel=2, + ) + return _VALIDATORS + elif name == "meta_schemas": + warnings.warn( + "Accessing jsonschema.validators.meta_schemas is deprecated. " + "Use jsonschema.validators.validator_for with a given schema.", + DeprecationWarning, + stacklevel=2, + ) + return _META_SCHEMAS + elif name == "RefResolver": + warnings.warn( + _RefResolver._DEPRECATION_MESSAGE, + DeprecationWarning, + stacklevel=2, + ) + return _RefResolver + raise AttributeError(f"module {__name__} has no attribute {name}") + + +def validates(version): + """ + Register the decorated validator for a ``version`` of the specification. + + Registered validators and their meta schemas will be considered when + parsing :kw:`$schema` keywords' URIs. + + Arguments: + + version (str): + + An identifier to use as the version's name + + Returns: + + collections.abc.Callable: + + a class decorator to decorate the validator with the version + """ + + def _validates(cls): + _VALIDATORS[version] = cls + meta_schema_id = cls.ID_OF(cls.META_SCHEMA) + _META_SCHEMAS[meta_schema_id] = cls + return cls + return _validates + + +def create( + meta_schema: referencing.jsonschema.ObjectSchema, + validators: ( + Mapping[str, _typing.SchemaKeywordValidator] + | Iterable[tuple[str, _typing.SchemaKeywordValidator]] + ) = (), + version: str | None = None, + type_checker: _types.TypeChecker = _types.draft202012_type_checker, + format_checker: _format.FormatChecker = _format.draft202012_format_checker, + id_of: _typing.id_of = referencing.jsonschema.DRAFT202012.id_of, + applicable_validators: _typing.ApplicableValidators = methodcaller( + "items", + ), +): + """ + Create a new validator class. + + Arguments: + + meta_schema: + + the meta schema for the new validator class + + validators: + + a mapping from names to callables, where each callable will + validate the schema property with the given name. + + Each callable should take 4 arguments: + + 1. a validator instance, + 2. the value of the property being validated within the + instance + 3. the instance + 4. the schema + + version: + + an identifier for the version that this validator class will + validate. If provided, the returned validator class will + have its ``__name__`` set to include the version, and also + will have `jsonschema.validators.validates` automatically + called for the given version. + + type_checker: + + a type checker, used when applying the :kw:`type` keyword. + + If unprovided, a `jsonschema.TypeChecker` will be created + with a set of default types typical of JSON Schema drafts. + + format_checker: + + a format checker, used when applying the :kw:`format` keyword. + + If unprovided, a `jsonschema.FormatChecker` will be created + with a set of default formats typical of JSON Schema drafts. + + id_of: + + A function that given a schema, returns its ID. + + applicable_validators: + + A function that, given a schema, returns the list of + applicable schema keywords and associated values + which will be used to validate the instance. + This is mostly used to support pre-draft 7 versions of JSON Schema + which specified behavior around ignoring keywords if they were + siblings of a ``$ref`` keyword. If you're not attempting to + implement similar behavior, you can typically ignore this argument + and leave it at its default. + + Returns: + + a new `jsonschema.protocols.Validator` class + """ + # preemptively don't shadow the `Validator.format_checker` local + format_checker_arg = format_checker + + specification = referencing.jsonschema.specification_with( + dialect_id=id_of(meta_schema) or "urn:unknown-dialect", + default=Specification.OPAQUE, + ) + + @define + class Validator: + + VALIDATORS = dict(validators) + META_SCHEMA = dict(meta_schema) + TYPE_CHECKER = type_checker + FORMAT_CHECKER = format_checker_arg + ID_OF = staticmethod(id_of) + + schema: referencing.jsonschema.Schema = field(repr=reprlib.repr) + _ref_resolver = field(default=None, repr=False, alias="resolver") + format_checker: _format.FormatChecker | None = field(default=None) + # TODO: include new meta-schemas added at runtime + _registry: referencing.jsonschema.SchemaRegistry = field( + default=SPECIFICATIONS, + converter=SPECIFICATIONS.combine, # type: ignore[misc] + kw_only=True, + repr=False, + ) + _resolver = field( + alias="_resolver", + default=None, + kw_only=True, + repr=False, + ) + + def __init_subclass__(cls): + warnings.warn( + ( + "Subclassing validator classes is not intended to " + "be part of their public API. A future version " + "will make doing so an error, as the behavior of " + "subclasses isn't guaranteed to stay the same " + "between releases of jsonschema. Instead, prefer " + "composition of validators, wrapping them in an object " + "owned entirely by the downstream library." + ), + DeprecationWarning, + stacklevel=2, + ) + + def evolve(self, **changes): + cls = self.__class__ + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=cls) + + for field in fields(cls): # noqa: F402 + if not field.init: + continue + attr_name = field.name + init_name = field.alias + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + cls.evolve = evolve + + def __attrs_post_init__(self): + if self._resolver is None: + self._resolver = self._registry.resolver_with_root( + resource=specification.create_resource(self.schema), + ) + + @classmethod + def check_schema(cls, schema, format_checker=_UNSET): + Validator = validator_for(cls.META_SCHEMA, default=cls) + if format_checker is _UNSET: + format_checker = Validator.FORMAT_CHECKER + validator = Validator( + schema=cls.META_SCHEMA, + format_checker=format_checker, + ) + for error in validator.iter_errors(schema): + raise exceptions.SchemaError.create_from(error) + + @property + def resolver(self): + warnings.warn( + ( + f"Accessing {self.__class__.__name__}.resolver is " + "deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing " + "library, which provides more compliant referencing " + "behavior as well as more flexible APIs for " + "customization." + ), + DeprecationWarning, + stacklevel=2, + ) + if self._ref_resolver is None: + self._ref_resolver = _RefResolver.from_schema( + self.schema, + id_of=id_of, + ) + return self._ref_resolver + + def evolve(self, **changes): + schema = changes.setdefault("schema", self.schema) + NewValidator = validator_for(schema, default=self.__class__) + + for (attr_name, init_name) in evolve_fields: + if init_name not in changes: + changes[init_name] = getattr(self, attr_name) + + return NewValidator(**changes) + + def iter_errors(self, instance, _schema=None): + if _schema is not None: + warnings.warn( + ( + "Passing a schema to Validator.iter_errors " + "is deprecated and will be removed in a future " + "release. Call validator.evolve(schema=new_schema)." + "iter_errors(...) instead." + ), + DeprecationWarning, + stacklevel=2, + ) + else: + _schema = self.schema + + if _schema is True: + return + elif _schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=_schema, + ) + return + + for k, v in applicable_validators(_schema): + validator = self.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(self, v, instance, _schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=_schema, + type_checker=self.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + yield error + + def descend( + self, + instance, + schema, + path=None, + schema_path=None, + resolver=None, + ): + if schema is True: + return + elif schema is False: + yield exceptions.ValidationError( + f"False schema does not allow {instance!r}", + validator=None, + validator_value=None, + instance=instance, + schema=schema, + ) + return + + if resolver is None: + resolver = self._resolver.in_subresource( + specification.create_resource(schema), + ) + evolved = self.evolve(schema=schema, _resolver=resolver) + + for k, v in applicable_validators(schema): + validator = evolved.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(evolved, v, instance, schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=schema, + type_checker=evolved.TYPE_CHECKER, + ) + if k not in {"if", "$ref"}: + error.schema_path.appendleft(k) + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error + + def validate(self, *args, **kwargs): + for error in self.iter_errors(*args, **kwargs): + raise error + + def is_type(self, instance, type): + try: + return self.TYPE_CHECKER.is_type(instance, type) + except exceptions.UndefinedTypeCheck: + raise exceptions.UnknownType(type, instance, self.schema) + + def _validate_reference(self, ref, instance): + if self._ref_resolver is None: + resolved = self._resolver.lookup(ref) + return self.descend( + instance, + resolved.contents, + resolver=resolved.resolver, + ) + else: + resolve = getattr(self._ref_resolver, "resolve", None) + if resolve is None: + with self._ref_resolver.resolving(ref) as resolved: + return self.descend(instance, resolved) + else: + scope, resolved = resolve(ref) + self._ref_resolver.push_scope(scope) + + try: + return self.descend(instance, resolved) + finally: + self._ref_resolver.pop_scope() + + def is_valid(self, instance, _schema=None): + if _schema is not None: + warnings.warn( + ( + "Passing a schema to Validator.is_valid is deprecated " + "and will be removed in a future release. Call " + "validator.evolve(schema=new_schema).is_valid(...) " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + self = self.evolve(schema=_schema) + + error = next(self.iter_errors(instance), None) + return error is None + + evolve_fields = [ + (field.name, field.alias) + for field in fields(Validator) + if field.init + ] + + if version is not None: + safe = version.title().replace(" ", "").replace("-", "") + Validator.__name__ = Validator.__qualname__ = f"{safe}Validator" + Validator = validates(version)(Validator) # type: ignore[misc] + + return Validator + + +def extend( + validator, + validators=(), + version=None, + type_checker=None, + format_checker=None, +): + """ + Create a new validator class by extending an existing one. + + Arguments: + + validator (jsonschema.protocols.Validator): + + an existing validator class + + validators (collections.abc.Mapping): + + a mapping of new validator callables to extend with, whose + structure is as in `create`. + + .. note:: + + Any validator callables with the same name as an + existing one will (silently) replace the old validator + callable entirely, effectively overriding any validation + done in the "parent" validator class. + + If you wish to instead extend the behavior of a parent's + validator callable, delegate and call it directly in + the new validator function by retrieving it using + ``OldValidator.VALIDATORS["validation_keyword_name"]``. + + version (str): + + a version for the new validator class + + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :kw:`type` keyword. + + If unprovided, the type checker of the extended + `jsonschema.protocols.Validator` will be carried along. + + format_checker (jsonschema.FormatChecker): + + a format checker, used when applying the :kw:`format` keyword. + + If unprovided, the format checker of the extended + `jsonschema.protocols.Validator` will be carried along. + + Returns: + + a new `jsonschema.protocols.Validator` class extending the one + provided + + .. note:: Meta Schemas + + The new validator class will have its parent's meta schema. + + If you wish to change or extend the meta schema in the new + validator class, modify ``META_SCHEMA`` directly on the returned + class. Note that no implicit copying is done, so a copy should + likely be made before modifying it, in order to not affect the + old validator. + """ + all_validators = dict(validator.VALIDATORS) + all_validators.update(validators) + + if type_checker is None: + type_checker = validator.TYPE_CHECKER + if format_checker is None: + format_checker = validator.FORMAT_CHECKER + return create( + meta_schema=validator.META_SCHEMA, + validators=all_validators, + version=version, + type_checker=type_checker, + format_checker=format_checker, + id_of=validator.ID_OF, + ) + + +Draft3Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-03/schema#", + ), + validators={ + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "dependencies": _legacy_validators.dependencies_draft3, + "disallow": _legacy_validators.disallow_draft3, + "divisibleBy": _validators.multipleOf, + "enum": _validators.enum, + "extends": _legacy_validators.extends_draft3, + "format": _validators.format, + "items": _legacy_validators.items_draft3_draft4, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maximum": _legacy_validators.maximum_draft3_draft4, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minimum": _legacy_validators.minimum_draft3_draft4, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "properties": _legacy_validators.properties_draft3, + "type": _legacy_validators.type_draft3, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft3_type_checker, + format_checker=_format.draft3_format_checker, + version="draft3", + id_of=referencing.jsonschema.DRAFT3.id_of, + applicable_validators=_legacy_validators.ignore_ref_siblings, +) + +Draft4Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-04/schema#", + ), + validators={ + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "allOf": _validators.allOf, + "anyOf": _validators.anyOf, + "dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, + "enum": _validators.enum, + "format": _validators.format, + "items": _legacy_validators.items_draft3_draft4, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maxProperties": _validators.maxProperties, + "maximum": _legacy_validators.maximum_draft3_draft4, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minProperties": _validators.minProperties, + "minimum": _legacy_validators.minimum_draft3_draft4, + "multipleOf": _validators.multipleOf, + "not": _validators.not_, + "oneOf": _validators.oneOf, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "properties": _validators.properties, + "required": _validators.required, + "type": _validators.type, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft4_type_checker, + format_checker=_format.draft4_format_checker, + version="draft4", + id_of=referencing.jsonschema.DRAFT4.id_of, + applicable_validators=_legacy_validators.ignore_ref_siblings, +) + +Draft6Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-06/schema#", + ), + validators={ + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "allOf": _validators.allOf, + "anyOf": _validators.anyOf, + "const": _validators.const, + "contains": _legacy_validators.contains_draft6_draft7, + "dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, + "enum": _validators.enum, + "exclusiveMaximum": _validators.exclusiveMaximum, + "exclusiveMinimum": _validators.exclusiveMinimum, + "format": _validators.format, + "items": _legacy_validators.items_draft6_draft7_draft201909, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maxProperties": _validators.maxProperties, + "maximum": _validators.maximum, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minProperties": _validators.minProperties, + "minimum": _validators.minimum, + "multipleOf": _validators.multipleOf, + "not": _validators.not_, + "oneOf": _validators.oneOf, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "properties": _validators.properties, + "propertyNames": _validators.propertyNames, + "required": _validators.required, + "type": _validators.type, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft6_type_checker, + format_checker=_format.draft6_format_checker, + version="draft6", + id_of=referencing.jsonschema.DRAFT6.id_of, + applicable_validators=_legacy_validators.ignore_ref_siblings, +) + +Draft7Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-07/schema#", + ), + validators={ + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "allOf": _validators.allOf, + "anyOf": _validators.anyOf, + "const": _validators.const, + "contains": _legacy_validators.contains_draft6_draft7, + "dependencies": _legacy_validators.dependencies_draft4_draft6_draft7, + "enum": _validators.enum, + "exclusiveMaximum": _validators.exclusiveMaximum, + "exclusiveMinimum": _validators.exclusiveMinimum, + "format": _validators.format, + "if": _validators.if_, + "items": _legacy_validators.items_draft6_draft7_draft201909, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maxProperties": _validators.maxProperties, + "maximum": _validators.maximum, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minProperties": _validators.minProperties, + "minimum": _validators.minimum, + "multipleOf": _validators.multipleOf, + "not": _validators.not_, + "oneOf": _validators.oneOf, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "properties": _validators.properties, + "propertyNames": _validators.propertyNames, + "required": _validators.required, + "type": _validators.type, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft7_type_checker, + format_checker=_format.draft7_format_checker, + version="draft7", + id_of=referencing.jsonschema.DRAFT7.id_of, + applicable_validators=_legacy_validators.ignore_ref_siblings, +) + +Draft201909Validator = create( + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2019-09/schema", + ), + validators={ + "$recursiveRef": _legacy_validators.recursiveRef, + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "allOf": _validators.allOf, + "anyOf": _validators.anyOf, + "const": _validators.const, + "contains": _validators.contains, + "dependentRequired": _validators.dependentRequired, + "dependentSchemas": _validators.dependentSchemas, + "enum": _validators.enum, + "exclusiveMaximum": _validators.exclusiveMaximum, + "exclusiveMinimum": _validators.exclusiveMinimum, + "format": _validators.format, + "if": _validators.if_, + "items": _legacy_validators.items_draft6_draft7_draft201909, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maxProperties": _validators.maxProperties, + "maximum": _validators.maximum, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minProperties": _validators.minProperties, + "minimum": _validators.minimum, + "multipleOf": _validators.multipleOf, + "not": _validators.not_, + "oneOf": _validators.oneOf, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "properties": _validators.properties, + "propertyNames": _validators.propertyNames, + "required": _validators.required, + "type": _validators.type, + "unevaluatedItems": _legacy_validators.unevaluatedItems_draft2019, + "unevaluatedProperties": _validators.unevaluatedProperties, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft201909_type_checker, + format_checker=_format.draft201909_format_checker, + version="draft2019-09", +) + +Draft202012Validator = create( + meta_schema=SPECIFICATIONS.contents( + "https://json-schema.org/draft/2020-12/schema", + ), + validators={ + "$dynamicRef": _validators.dynamicRef, + "$ref": _validators.ref, + "additionalItems": _validators.additionalItems, + "additionalProperties": _validators.additionalProperties, + "allOf": _validators.allOf, + "anyOf": _validators.anyOf, + "const": _validators.const, + "contains": _validators.contains, + "dependentRequired": _validators.dependentRequired, + "dependentSchemas": _validators.dependentSchemas, + "enum": _validators.enum, + "exclusiveMaximum": _validators.exclusiveMaximum, + "exclusiveMinimum": _validators.exclusiveMinimum, + "format": _validators.format, + "if": _validators.if_, + "items": _validators.items, + "maxItems": _validators.maxItems, + "maxLength": _validators.maxLength, + "maxProperties": _validators.maxProperties, + "maximum": _validators.maximum, + "minItems": _validators.minItems, + "minLength": _validators.minLength, + "minProperties": _validators.minProperties, + "minimum": _validators.minimum, + "multipleOf": _validators.multipleOf, + "not": _validators.not_, + "oneOf": _validators.oneOf, + "pattern": _validators.pattern, + "patternProperties": _validators.patternProperties, + "prefixItems": _validators.prefixItems, + "properties": _validators.properties, + "propertyNames": _validators.propertyNames, + "required": _validators.required, + "type": _validators.type, + "unevaluatedItems": _validators.unevaluatedItems, + "unevaluatedProperties": _validators.unevaluatedProperties, + "uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft202012_type_checker, + format_checker=_format.draft202012_format_checker, + version="draft2020-12", +) + +_LATEST_VERSION = Draft202012Validator + + +class _RefResolver: + """ + Resolve JSON References. + + Arguments: + + base_uri (str): + + The URI of the referring document + + referrer: + + The actual referring document + + store (dict): + + A mapping from URIs to documents to cache + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + handlers (dict): + + A mapping from URI schemes to functions that should be used + to retrieve them + + urljoin_cache (:func:`functools.lru_cache`): + + A cache that will be used for caching the results of joining + the resolution scope to subscopes. + + remote_cache (:func:`functools.lru_cache`): + + A cache that will be used for caching the results of + resolved remote URLs. + + Attributes: + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + .. deprecated:: v4.18.0 + + ``RefResolver`` has been deprecated in favor of `referencing`. + """ + + _DEPRECATION_MESSAGE = ( + "jsonschema.RefResolver is deprecated as of v4.18.0, in favor of the " + "https://github.com/python-jsonschema/referencing library, which " + "provides more compliant referencing behavior as well as more " + "flexible APIs for customization. A future release will remove " + "RefResolver. Please file a feature request (on referencing) if you " + "are missing an API for the kind of customization you need." + ) + + def __init__( + self, + base_uri, + referrer, + store=HashTrieMap(), + cache_remote=True, + handlers=(), + urljoin_cache=None, + remote_cache=None, + ): + if urljoin_cache is None: + urljoin_cache = lru_cache(1024)(urljoin) + if remote_cache is None: + remote_cache = lru_cache(1024)(self.resolve_from_url) + + self.referrer = referrer + self.cache_remote = cache_remote + self.handlers = dict(handlers) + + self._scopes_stack = [base_uri] + + self.store = _utils.URIDict( + (uri, each.contents) for uri, each in SPECIFICATIONS.items() + ) + self.store.update( + (id, each.META_SCHEMA) for id, each in _META_SCHEMAS.items() + ) + self.store.update(store) + self.store.update( + (schema["$id"], schema) + for schema in store.values() + if isinstance(schema, Mapping) and "$id" in schema + ) + self.store[base_uri] = referrer + + self._urljoin_cache = urljoin_cache + self._remote_cache = remote_cache + + @classmethod + def from_schema( # noqa: D417 + cls, + schema, + id_of=referencing.jsonschema.DRAFT202012.id_of, + *args, + **kwargs, + ): + """ + Construct a resolver from a JSON schema object. + + Arguments: + + schema: + + the referring schema + + Returns: + + `_RefResolver` + """ + return cls(base_uri=id_of(schema) or "", referrer=schema, *args, **kwargs) # noqa: B026, E501 + + def push_scope(self, scope): + """ + Enter a given sub-scope. + + Treats further dereferences as being performed underneath the + given scope. + """ + self._scopes_stack.append( + self._urljoin_cache(self.resolution_scope, scope), + ) + + def pop_scope(self): + """ + Exit the most recent entered scope. + + Treats further dereferences as being performed underneath the + original scope. + + Don't call this method more times than `push_scope` has been + called. + """ + try: + self._scopes_stack.pop() + except IndexError: + raise exceptions._RefResolutionError( + "Failed to pop the scope from an empty stack. " + "`pop_scope()` should only be called once for every " + "`push_scope()`", + ) + + @property + def resolution_scope(self): + """ + Retrieve the current resolution scope. + """ + return self._scopes_stack[-1] + + @property + def base_uri(self): + """ + Retrieve the current base URI, not including any fragment. + """ + uri, _ = urldefrag(self.resolution_scope) + return uri + + @contextlib.contextmanager + def in_scope(self, scope): + """ + Temporarily enter the given scope for the duration of the context. + + .. deprecated:: v4.0.0 + """ + warnings.warn( + "jsonschema.RefResolver.in_scope is deprecated and will be " + "removed in a future release.", + DeprecationWarning, + stacklevel=3, + ) + self.push_scope(scope) + try: + yield + finally: + self.pop_scope() + + @contextlib.contextmanager + def resolving(self, ref): + """ + Resolve the given ``ref`` and enter its resolution scope. + + Exits the scope on exit of this context manager. + + Arguments: + + ref (str): + + The reference to resolve + """ + url, resolved = self.resolve(ref) + self.push_scope(url) + try: + yield resolved + finally: + self.pop_scope() + + def _find_in_referrer(self, key): + return self._get_subschemas_cache()[key] + + @lru_cache # noqa: B019 + def _get_subschemas_cache(self): + cache = {key: [] for key in _SUBSCHEMAS_KEYWORDS} + for keyword, subschema in _search_schema( + self.referrer, _match_subschema_keywords, + ): + cache[keyword].append(subschema) + return cache + + @lru_cache # noqa: B019 + def _find_in_subschemas(self, url): + subschemas = self._get_subschemas_cache()["$id"] + if not subschemas: + return None + uri, fragment = urldefrag(url) + for subschema in subschemas: + target_uri = self._urljoin_cache( + self.resolution_scope, subschema["$id"], + ) + if target_uri.rstrip("/") == uri.rstrip("/"): + if fragment: + subschema = self.resolve_fragment(subschema, fragment) + self.store[url] = subschema + return url, subschema + return None + + def resolve(self, ref): + """ + Resolve the given reference. + """ + url = self._urljoin_cache(self.resolution_scope, ref).rstrip("/") + + match = self._find_in_subschemas(url) + if match is not None: + return match + + return url, self._remote_cache(url) + + def resolve_from_url(self, url): + """ + Resolve the given URL. + """ + url, fragment = urldefrag(url) + if not url: + url = self.base_uri + + try: + document = self.store[url] + except KeyError: + try: + document = self.resolve_remote(url) + except Exception as exc: + raise exceptions._RefResolutionError(exc) + + return self.resolve_fragment(document, fragment) + + def resolve_fragment(self, document, fragment): + """ + Resolve a ``fragment`` within the referenced ``document``. + + Arguments: + + document: + + The referent document + + fragment (str): + + a URI fragment to resolve within it + """ + fragment = fragment.lstrip("/") + + if not fragment: + return document + + if document is self.referrer: + find = self._find_in_referrer + else: + + def find(key): + yield from _search_schema(document, _match_keyword(key)) + + for keyword in ["$anchor", "$dynamicAnchor"]: + for subschema in find(keyword): + if fragment == subschema[keyword]: + return subschema + for keyword in ["id", "$id"]: + for subschema in find(keyword): + if "#" + fragment == subschema[keyword]: + return subschema + + # Resolve via path + parts = unquote(fragment).split("/") if fragment else [] + for part in parts: + part = part.replace("~1", "/").replace("~0", "~") + + if isinstance(document, Sequence): + try: # noqa: SIM105 + part = int(part) + except ValueError: + pass + try: + document = document[part] + except (TypeError, LookupError): + raise exceptions._RefResolutionError( + f"Unresolvable JSON pointer: {fragment!r}", + ) + + return document + + def resolve_remote(self, uri): + """ + Resolve a remote ``uri``. + + If called directly, does not check the store first, but after + retrieving the document at the specified URI it will be saved in + the store if :attr:`cache_remote` is True. + + .. note:: + + If the requests_ library is present, ``jsonschema`` will use it to + request the remote ``uri``, so that the correct encoding is + detected and used. + + If it isn't, or if the scheme of the ``uri`` is not ``http`` or + ``https``, UTF-8 is assumed. + + Arguments: + + uri (str): + + The URI to resolve + + Returns: + + The retrieved document + + .. _requests: https://pypi.org/project/requests/ + """ + try: + import requests + except ImportError: + requests = None + + scheme = urlsplit(uri).scheme + + if scheme in self.handlers: + result = self.handlers[scheme](uri) + elif scheme in ["http", "https"] and requests: + # Requests has support for detecting the correct encoding of + # json over http + result = requests.get(uri).json() + else: + # Otherwise, pass off to urllib and assume utf-8 + with urlopen(uri) as url: + result = json.loads(url.read().decode("utf-8")) + + if self.cache_remote: + self.store[uri] = result + return result + + +_SUBSCHEMAS_KEYWORDS = ("$id", "id", "$anchor", "$dynamicAnchor") + + +def _match_keyword(keyword): + + def matcher(value): + if keyword in value: + yield value + + return matcher + + +def _match_subschema_keywords(value): + for keyword in _SUBSCHEMAS_KEYWORDS: + if keyword in value: + yield keyword, value + + +def _search_schema(schema, matcher): + """Breadth-first search routine.""" + values = deque([schema]) + while values: + value = values.pop() + if not isinstance(value, dict): + continue + yield from matcher(value) + values.extendleft(value.values()) + + +def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417 + """ + Validate an instance under the given schema. + + >>> validate([2, 3, 4], {"maxItems": 2}) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + :func:`~jsonschema.validators.validate` will first verify that the + provided schema is itself valid, since not doing so can lead to less + obvious error messages and fail in less obvious or consistent ways. + + If you know you have a valid schema already, especially + if you intend to validate multiple instances with + the same schema, you likely would prefer using the + `jsonschema.protocols.Validator.validate` method directly on a + specific validator (e.g. ``Draft202012Validator.validate``). + + + Arguments: + + instance: + + The instance to validate + + schema: + + The schema to validate with + + cls (jsonschema.protocols.Validator): + + The class that will be used to validate the instance. + + If the ``cls`` argument is not provided, two things will happen + in accordance with the specification. First, if the schema has a + :kw:`$schema` keyword containing a known meta-schema [#]_ then the + proper validator will be used. The specification recommends that + all schemas contain :kw:`$schema` properties for this reason. If no + :kw:`$schema` property is found, the default validator class is the + latest released draft. + + Any other provided positional and keyword arguments will be passed + on when instantiating the ``cls``. + + Raises: + + `jsonschema.exceptions.ValidationError`: + + if the instance is invalid + + `jsonschema.exceptions.SchemaError`: + + if the schema itself is invalid + + .. rubric:: Footnotes + .. [#] known by a validator registered with + `jsonschema.validators.validates` + """ + if cls is None: + cls = validator_for(schema) + + cls.check_schema(schema) + validator = cls(schema, *args, **kwargs) + error = exceptions.best_match(validator.iter_errors(instance)) + if error is not None: + raise error + + +def validator_for(schema, default=_UNSET): + """ + Retrieve the validator class appropriate for validating the given schema. + + Uses the :kw:`$schema` keyword that should be present in the given + schema to look up the appropriate validator class. + + Arguments: + + schema (collections.abc.Mapping or bool): + + the schema to look at + + default: + + the default to return if the appropriate validator class + cannot be determined. + + If unprovided, the default is to return the latest supported + draft. + + Examples: + + The :kw:`$schema` JSON Schema keyword will control which validator + class is returned: + + >>> schema = { + ... "$schema": "https://json-schema.org/draft/2020-12/schema", + ... "type": "integer", + ... } + >>> jsonschema.validators.validator_for(schema) + <class 'jsonschema.validators.Draft202012Validator'> + + + Here, a draft 7 schema instead will return the draft 7 validator: + + >>> schema = { + ... "$schema": "http://json-schema.org/draft-07/schema#", + ... "type": "integer", + ... } + >>> jsonschema.validators.validator_for(schema) + <class 'jsonschema.validators.Draft7Validator'> + + + Schemas with no ``$schema`` keyword will fallback to the default + argument: + + >>> schema = {"type": "integer"} + >>> jsonschema.validators.validator_for( + ... schema, default=Draft7Validator, + ... ) + <class 'jsonschema.validators.Draft7Validator'> + + or if none is provided, to the latest version supported. + Always including the keyword when authoring schemas is highly + recommended. + + """ + DefaultValidator = _LATEST_VERSION if default is _UNSET else default + + if schema is True or schema is False or "$schema" not in schema: + return DefaultValidator + if schema["$schema"] not in _META_SCHEMAS and default is _UNSET: + warn( + ( + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future." + ), + DeprecationWarning, + stacklevel=2, + ) + return _META_SCHEMAS.get(schema["$schema"], DefaultValidator) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c550a59 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,153 @@ +[build-system] +requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "vcs" + +[project] +name = "jsonschema" +description = "An implementation of JSON Schema validation for Python" +requires-python = ">=3.8" +license = {text = "MIT"} +keywords = ["validation", "data validation", "jsonschema", "json"] +authors = [ + {email = "Julian+jsonschema@GrayVines.com"}, + {name = "Julian Berman"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version", "readme"] + +dependencies = [ + "attrs>=22.2.0", + "jsonschema-specifications>=2023.03.6", + "referencing>=0.25.3", + "rpds-py>=0.7.1", + + "importlib_resources>=1.4.0;python_version<'3.9'", + "pkgutil_resolve_name>=1.3.10;python_version<'3.9'", +] + +[project.optional-dependencies] +format = [ + "fqdn", + "idna", + "isoduration", + "jsonpointer>1.13", + "rfc3339-validator", + "rfc3987", + "uri_template", + "webcolors>=1.11", +] +format-nongpl = [ + "fqdn", + "idna", + "isoduration", + "jsonpointer>1.13", + "rfc3339-validator", + "rfc3986-validator>0.1.0", + "uri_template", + "webcolors>=1.11", +] + +[project.scripts] +jsonschema = "jsonschema.cli:main" + +[project.urls] +Homepage = "https://github.com/python-jsonschema/jsonschema" +Documentation = "https://python-jsonschema.readthedocs.io/" +Issues = "https://github.com/python-jsonschema/jsonschema/issues/" +Funding = "https://github.com/sponsors/Julian" +Tidelift = "https://tidelift.com/subscription/pkg/pypi-jsonschema?utm_source=pypi-jsonschema&utm_medium=referral&utm_campaign=pypi-link" +Changelog = "https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst" +Source = "https://github.com/python-jsonschema/jsonschema" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.rst" +end-before = ".. start cut from PyPI" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.rst" +start-after = ".. end cut from PyPI\n\n\n" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ + + +Release Information +------------------- + +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "CHANGELOG.rst" +pattern = "(^v.+?)\nv" + +[tool.isort] +combine_as_imports = true +from_first = true +include_trailing_comma = true +multi_line_output = 3 + +[tool.mypy] +ignore_missing_imports = true +show_error_codes = true + +[tool.ruff] +line-length = 79 +target-version = "py38" +select = ["B", "D", "D204", "E", "F", "Q", "SIM", "UP", "W"] +ignore = [ + # It's totally OK to call functions for default arguments. + "B008", + # raise SomeException(...) is fine. + "B904", + # It's fine to not have docstrings for magic methods. + "D105", + # __init__ especially doesn't need a docstring + "D107", + # This rule makes diffs uglier when expanding docstrings (and it's uglier) + "D200", + # No blank lines before docstrings. + "D203", + # Start docstrings on the second line. + "D212", + # This rule misses sassy docstrings ending with ! or ?. + "D400", + # Section headers should end with a colon not a newline + "D406", + # Underlines aren't needed + "D407", + # Plz spaces after section headers + "D412", +] +extend-exclude = ["json"] + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" + +[tool.ruff.per-file-ignores] +"docs/*" = ["D"] +"jsonschema/cli.py" = ["D", "SIM", "UP"] +"jsonschema/_utils.py" = ["D"] +"jsonschema/benchmarks/*" = ["D"] +"jsonschema/tests/*" = ["D", "SIM"] + +[tool.ruff.pyupgrade] +# We support 3.8 + 3.9 +keep-runtime-typing = true @@ -1,9 +1,113 @@ [tox] -minversion = 1.6 -envlist = sanity +min_version = 4.0.9 +envlist = + py{38,39,310,311,py3}-{noextra,format,formatnongpl}-{build,tests} + {noextra,format,formatnongpl}-audit + readme + secrets + style + typing + docs-{dirhtml,doctest,linkcheck,spelling,style} skipsdist = True -[testenv:sanity] -# used just for validating the structure of the test case files themselves -deps = jsonschema==4.17.3 -commands = {envpython} bin/jsonschema_suite check +[testenv] +changedir = {envtmpdir} +passenv = CI, GITHUB_STEP_SUMMARY, PYTHONUTF8 +setenv = + PIP_DISABLE_PIP_VERSION_CHECK = 1 + + JSON_SCHEMA_TEST_SUITE = {toxinidir}/json + + coverage,ghcoverage: MAYBE_COVERAGE=coverage run -m + coverage,ghcoverage: COVERAGE_RCFILE={toxinidir}/.coveragerc + coverage,ghcoverage: COVERAGE_DEBUG_FILE={envtmpdir}/coverage-debug + coverage,ghcoverage: COVERAGE_FILE={envtmpdir}/coverage-data +allowlist_externals = + mkdir + sh +commands = + noextra: {envpython} -m pip install --quiet {toxinidir} + format,perf: {envpython} -m pip install --quiet '{toxinidir}[format]' + formatnongpl: {envpython} -m pip install --quiet '{toxinidir}[format-nongpl]' + + audit: {envpython} -m pip_audit + audit-formatnongpl: {envpython} -m piplicenses {posargs} --ignore-packages pip-requirements-parser pip_audit pip-api --allow-only="Apache Software License;ISC License (ISCL);MIT License;BSD License;Mozilla Public License 2.0 (MPL 2.0);Python Software Foundation License" + + build: {envpython} -m build {toxinidir} --outdir {envtmpdir}/dist + + tests,coverage,ghcoverage: {envpython} -Werror -m {env:MAYBE_COVERAGE:} virtue {posargs:jsonschema} + tests: {envpython} -m doctest {toxinidir}/README.rst + + coverage: {envpython} -m coverage report --show-missing + coverage: {envpython} -m coverage html --directory={envtmpdir}/htmlcov + ghcoverage: sh {toxinidir}/.github/coverage.sh "{envpython}" + + perf-create: {envpython} {toxinidir}/jsonschema/benchmarks/validator_creation.py {posargs:--output {envtmpdir}/bench-create.json} + perf-232: {envpython} {toxinidir}/jsonschema/benchmarks/issue232.py {posargs:--output {envtmpdir}/bench-issue232.json} + perf-suite: {envpython} {toxinidir}/jsonschema/benchmarks/json_schema_test_suite.py --inherit-environ JSON_SCHEMA_TEST_SUITE {posargs:--output {envtmpdir}/bench-json_schema_test_suite.json} +deps = + audit: pip-audit + audit-formatnongpl: pip-licenses + + build: build + + perf: pyperf + + tests,coverage,ghcoverage: virtue + + coverage,ghcoverage: coverage>=7.0.0b1 + +[testenv:bandit] +deps = bandit +commands = {envbindir}/bandit --recursive {toxinidir}/jsonschema + +[testenv:readme] +deps = + build + docutils + twine +commands = + {envpython} -m build --outdir {envtmpdir}/dist {toxinidir} + {envpython} -m twine check {envtmpdir}/dist/* + {envbindir}/rst2html5.py --halt=warning {toxinidir}/CHANGELOG.rst /dev/null + +[testenv:secrets] +deps = detect-secrets +commands = {envbindir}/detect-secrets scan {toxinidir} + +[testenv:style] +deps = ruff +commands = {envpython} -m ruff check {toxinidir} + +[testenv:typing] +skip_install = true +deps = + # FIXME: Why are we repeating dependencies here? + attrs + mypy + types-requests +commands = {envpython} -m mypy --config {toxinidir}/pyproject.toml {posargs} {toxinidir}/jsonschema + +[testenv:docs-dirhtml] +commands = {envpython} -m sphinx -b dirhtml {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -q -T -W} +deps = + -r{toxinidir}/docs/requirements.txt + +[testenv:docs-doctest] +commands = {envpython} -m sphinx -b doctest {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -q -T -W} +deps = {[testenv:docs-dirhtml]deps} + +[testenv:docs-linkcheck] +commands = {envpython} -m sphinx -b linkcheck {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -q -T -W} +deps = {[testenv:docs-dirhtml]deps} + +[testenv:docs-spelling] +commands = {envpython} -m sphinx -b spelling {toxinidir}/docs/ {envtmpdir}/build {posargs:-a -n -T -W} +deps = {[testenv:docs-dirhtml]deps} + +[testenv:docs-style] +commands = doc8 --max-line-length 1000 {posargs} {toxinidir}/docs +deps = + doc8 + pygments + pygments-github-lexers |