diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a01200b60..1890cf19f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,44 +7,36 @@ on:
tags:
- "v*"
pull_request:
- release:
- types: [published]
schedule:
# Daily at 3:21
- cron: "21 3 * * *"
workflow_dispatch:
-env:
- PIP_DISABLE_PIP_VERSION_CHECK: "1"
- PIP_NO_PYTHON_VERSION_WARNING: "1"
+permissions: {}
jobs:
- pre-commit:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: "3.x"
- - uses: pre-commit/action@v3.0.1
-
list:
runs-on: ubuntu-latest
outputs:
noxenvs: ${{ steps.noxenvs-matrix.outputs.noxenvs }}
steps:
- uses: actions/checkout@v4
- - name: Set up nox
- uses: wntrblm/nox@2024.04.15
+ with:
+ persist-credentials: false
+ - name: Set up uv
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb
+ with:
+ enable-cache: ${{ github.ref_type != 'tag' }} # zizmor: ignore[cache-poisoning]
- id: noxenvs-matrix
run: |
echo >>$GITHUB_OUTPUT noxenvs=$(
- nox --list-sessions --json | jq '[.[].session]'
+ uvx nox --list-sessions --json | jq '[.[].session]'
)
ci:
needs: list
runs-on: ${{ matrix.os }}
+
strategy:
fail-fast: false
matrix:
@@ -53,12 +45,24 @@ jobs:
posargs: [""]
include:
- os: ubuntu-latest
- noxenv: "tests-3.11(format)"
+ noxenv: "tests-3.13(format)"
posargs: coverage github
- os: ubuntu-latest
- noxenv: "tests-3.11(no-extras)"
+ noxenv: "tests-3.13(no-extras)"
posargs: coverage github
exclude:
+ - os: macos-latest
+ noxenv: "docs(dirhtml)"
+ - os: macos-latest
+ noxenv: "docs(doctest)"
+ - os: macos-latest
+ noxenv: "docs(linkcheck)"
+ - os: macos-latest
+ noxenv: "docs(man)"
+ - os: macos-latest
+ noxenv: "docs(spelling)"
+ - os: macos-latest
+ noxenv: "docs(style)"
- os: windows-latest
noxenv: "docs(dirhtml)"
- os: windows-latest
@@ -72,6 +76,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libenchant-2-dev
if: runner.os == 'Linux' && startsWith(matrix.noxenv, 'docs')
@@ -82,21 +88,24 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: |
- 3.8
3.9
3.10
3.11
3.12
3.13
- pypy3.10
+ pypy3.11
allow-prereleases: true
- - name: Set up nox
- uses: wntrblm/nox@2024.04.15
- name: Enable UTF-8 on Windows
run: echo "PYTHONUTF8=1" >> $env:GITHUB_ENV
if: runner.os == 'Windows' && startsWith(matrix.noxenv, 'tests')
+
+ - name: Set up uv
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb
+ with:
+ enable-cache: true
+
- name: Run nox
- run: nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }}
+ run: uvx nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }} # zizmor: ignore[template-injection]
packaging:
needs: ci
@@ -104,6 +113,7 @@ jobs:
environment:
name: PyPI
url: https://pypi.org/p/jsonschema
+
permissions:
contents: write
id-token: write
@@ -112,20 +122,21 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Set up Python
- uses: actions/setup-python@v5
+ persist-credentials: false
+ - name: Set up uv
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb
with:
- python-version: "3.x"
- - name: Install dependencies
- run: python -m pip install build
- - name: Create packages
- run: python -m build .
+ enable-cache: true
+
+ - name: Build our distributions
+ run: uv run --with 'build[uv]' -m build --installer=uv
+
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
- uses: pypa/gh-action-pypi-publish@release/v1
- - name: Create a Release
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
+ - name: Create a GitHub Release
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
with:
files: |
dist/*
diff --git a/.github/workflows/documentation-links.yml b/.github/workflows/documentation-links.yml
index 5757faf47..2e02d13cb 100644
--- a/.github/workflows/documentation-links.yml
+++ b/.github/workflows/documentation-links.yml
@@ -1,6 +1,6 @@
name: Read the Docs Pull Request Preview
on:
- pull_request_target:
+ pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- opened
@@ -11,6 +11,6 @@ jobs:
documentation-links:
runs-on: ubuntu-latest
steps:
- - uses: readthedocs/actions/preview@v1
+ - uses: readthedocs/actions/preview@b8bba1484329bda1a3abe986df7ebc80a8950333
with:
project-slug: "python-jsonschema"
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
deleted file mode 100644
index eaabb4fb5..000000000
--- a/.github/workflows/fuzz.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-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@v4
- if: failure() && steps.build.outcome == 'success'
- with:
- name: artifacts
- path: ./out/artifacts
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
new file mode 100644
index 000000000..bbdd9c791
--- /dev/null
+++ b/.github/workflows/zizmor.yml
@@ -0,0 +1,35 @@
+name: GitHub Actions Security Analysis with zizmor 🌈
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["**"]
+
+jobs:
+ zizmor:
+ name: Run zizmor
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb
+
+ - name: Run zizmor 🌈
+ run: uvx zizmor --format=sarif .github > results.sarif
+
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload SARIF file
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
+ category: zizmor
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a42390115..eaa2cc878 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: json/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v5.0.0
hooks:
- id: check-ast
- id: check-json
@@ -16,16 +16,7 @@ repos:
args: [--fix, lf]
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.5.0"
+ rev: "v0.11.11"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- - repo: https://github.com/PyCQA/isort
- rev: 5.13.2
- hooks:
- - id: isort
- - repo: https://github.com/pre-commit/mirrors-prettier
- rev: "v4.0.0-alpha.8"
- hooks:
- - id: prettier
- exclude: "^jsonschema/benchmarks/issue232/issue.json$"
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a7b9d86eb..ce75f77ed 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,9 @@
+v4.24.0
+=======
+
+* Fix improper handling of ``unevaluatedProperties`` in the presence of ``additionalProperties`` (#1351).
+* Support for Python 3.8 has been dropped, as it is end-of-life.
+
v4.23.0
=======
diff --git a/README.rst b/README.rst
index 4889438ab..29381f41e 100644
--- a/README.rst
+++ b/README.rst
@@ -134,8 +134,6 @@ I'm Julian Berman.
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 `_.
And for companies who appreciate ``jsonschema`` and its continued support and growth, ``jsonschema`` is also now supportable via `TideLift `_.
diff --git a/docs/referencing.rst b/docs/referencing.rst
index 425cb13a8..223b03363 100644
--- a/docs/referencing.rst
+++ b/docs/referencing.rst
@@ -220,6 +220,7 @@ Here for instance we retrieve YAML documents in a way similar to the `above [`_ file in the check-jsonschema repository.
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.
diff --git a/docs/requirements.in b/docs/requirements.in
index 5a68be72b..ae66984ae 100644
--- a/docs/requirements.in
+++ b/docs/requirements.in
@@ -1,4 +1,4 @@
-file:.#egg=jsonschema
+file:.
furo
lxml
sphinx!=7.2.5
@@ -8,6 +8,3 @@ sphinx-copybutton
sphinx-json-schema-spec
sphinxcontrib-spelling
sphinxext-opengraph
-
-# Until pyenchant/pyenchant#302 is released...
-pyenchant>=3.3.0rc1
diff --git a/docs/requirements.txt b/docs/requirements.txt
index ca1958ab6..8d5743117 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,74 +1,72 @@
-#
-# This file is autogenerated by pip-compile with Python 3.12
-# by the following command:
-#
-# pip-compile --strip-extras docs/requirements.in
-#
-alabaster==0.7.16
+# This file was autogenerated by uv via the following command:
+# uv pip compile --output-file /Users/julian/Development/jsonschema/docs/requirements.txt docs/requirements.in
+alabaster==1.0.0
# via sphinx
-astroid==3.2.2
+astroid==3.3.9
# via sphinx-autoapi
-attrs==23.2.0
+attrs==25.3.0
# via
# jsonschema
# referencing
-babel==2.15.0
+babel==2.17.0
# via sphinx
-beautifulsoup4==4.12.3
+beautifulsoup4==4.13.3
# via furo
-certifi==2024.6.2
+certifi==2025.1.31
# via requests
-charset-normalizer==3.3.2
+charset-normalizer==3.4.1
# via requests
docutils==0.21.2
# via sphinx
-furo==2024.5.6
+furo==2024.8.6
# via -r docs/requirements.in
-idna==3.7
+idna==3.10
# via requests
imagesize==1.4.1
# via sphinx
-jinja2==3.1.4
+jinja2==3.1.6
# via
# sphinx
# sphinx-autoapi
-file:.#egg=jsonschema
+jsonschema @ file:.
# via -r docs/requirements.in
-jsonschema-specifications==2023.12.1
+jsonschema-specifications==2024.10.1
# via jsonschema
-lxml==5.2.2
+lxml==5.3.1
# via
# -r docs/requirements.in
# sphinx-json-schema-spec
-markupsafe==2.1.5
+markupsafe==3.0.2
# via jinja2
-packaging==24.1
+packaging==24.2
# via sphinx
-pyenchant==3.3.0rc1
- # via
- # -r docs/requirements.in
- # sphinxcontrib-spelling
-pygments==2.18.0
+pyenchant==3.2.2
+ # via sphinxcontrib-spelling
+pygments==2.19.1
# via
# furo
# sphinx
-pyyaml==6.0.1
+pyyaml==6.0.2
# via sphinx-autoapi
-referencing==0.35.1
+referencing==0.36.2
# via
# jsonschema
# jsonschema-specifications
requests==2.32.3
+ # via
+ # sphinx
+ # sphinxcontrib-spelling
+roman-numerals-py==3.1.0
# via sphinx
-rpds-py==0.18.1
+rpds-py==0.24.0
# via
# jsonschema
# referencing
snowballstemmer==2.2.0
# via sphinx
-soupsieve==2.5
+soupsieve==2.6
# via beautifulsoup4
-sphinx==7.3.7
+sphinx==8.2.3
# via
# -r docs/requirements.in
# furo
@@ -79,31 +77,33 @@ sphinx==7.3.7
# sphinx-json-schema-spec
# sphinxcontrib-spelling
# sphinxext-opengraph
-sphinx-autoapi==3.1.1
+sphinx-autoapi==3.6.0
# via -r docs/requirements.in
-sphinx-autodoc-typehints==2.1.1
+sphinx-autodoc-typehints==3.1.0
# via -r docs/requirements.in
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-copybutton==0.5.2
# via -r docs/requirements.in
-sphinx-json-schema-spec==2024.1.1
+sphinx-json-schema-spec==2025.1.1
# via -r docs/requirements.in
-sphinxcontrib-applehelp==1.0.8
+sphinxcontrib-applehelp==2.0.0
# via sphinx
-sphinxcontrib-devhelp==1.0.6
+sphinxcontrib-devhelp==2.0.0
# via sphinx
-sphinxcontrib-htmlhelp==2.0.5
+sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
-sphinxcontrib-qthelp==1.0.7
+sphinxcontrib-qthelp==2.0.0
# via sphinx
-sphinxcontrib-serializinghtml==1.1.10
+sphinxcontrib-serializinghtml==2.0.0
# via sphinx
-sphinxcontrib-spelling==8.0.0
+sphinxcontrib-spelling==8.0.1
# via -r docs/requirements.in
sphinxext-opengraph==0.9.1
# via -r docs/requirements.in
-urllib3==2.2.2
+typing-extensions==4.13.0
+ # via beautifulsoup4
+urllib3==2.3.0
# via requests
diff --git a/json/.github/workflows/annotation-tests.yml b/json/.github/workflows/annotation-tests.yml
new file mode 100644
index 000000000..16de7b169
--- /dev/null
+++ b/json/.github/workflows/annotation-tests.yml
@@ -0,0 +1,21 @@
+name: Validate annotation tests
+
+on:
+ pull_request:
+ paths:
+ - "annotations/**"
+
+jobs:
+ annotate:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Deno
+ uses: denoland/setup-deno@v2
+ with:
+ deno-version: "2.x"
+
+ - name: Validate annotation tests
+ run: deno --node-modules-dir=auto --allow-read --no-prompt bin/annotation-tests.ts
diff --git a/json/.github/workflows/pr-dependencies.yml b/json/.github/workflows/pr-dependencies.yml
new file mode 100644
index 000000000..34a231dcb
--- /dev/null
+++ b/json/.github/workflows/pr-dependencies.yml
@@ -0,0 +1,12 @@
+name: Check PR Dependencies
+
+on: pull_request
+
+jobs:
+ check_dependencies:
+ runs-on: ubuntu-latest
+ name: Check Dependencies
+ steps:
+ - uses: gregsdennis/dependencies-action@main
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/json/.github/workflows/show_specification_annotations.yml b/json/.github/workflows/show_specification_annotations.yml
new file mode 100644
index 000000000..f7d7b398b
--- /dev/null
+++ b/json/.github/workflows/show_specification_annotations.yml
@@ -0,0 +1,21 @@
+name: Show Specification Annotations
+
+on:
+ pull_request:
+ paths:
+ - 'tests/**'
+
+jobs:
+ annotate:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Generate Annotations
+ run: pip install uritemplate && bin/annotate-specification-links
diff --git a/json/README.md b/json/README.md
index bfdcb501c..9f4c516db 100644
--- a/json/README.md
+++ b/json/README.md
@@ -227,6 +227,7 @@ This suite is being used by:
### C++
+* [Blaze](https://github.com/sourcemeta/blaze)
* [Modern C++ JSON schema validator](https://github.com/pboettch/json-schema-validator)
* [Valijson](https://github.com/tristanpenman/valijson)
@@ -293,7 +294,7 @@ Node-specific support is maintained in a [separate repository](https://github.co
### .NET
-* [JsonSchema.Net](https://github.com/gregsdennis/json-everything)
+* [JsonSchema.Net](https://github.com/json-everything/json-everything)
* [Newtonsoft.Json.Schema](https://github.com/JamesNK/Newtonsoft.Json.Schema)
### Perl
@@ -339,6 +340,7 @@ Node-specific support is maintained in a [separate repository](https://github.co
### Swift
* [JSONSchema](https://github.com/kylef/JSONSchema.swift)
+* [swift-json-schema](https://github.com/ajevans99/swift-json-schema)
If you use it as well, please fork and send a pull request adding yourself to
the list :).
diff --git a/json/annotations/README.md b/json/annotations/README.md
new file mode 100644
index 000000000..69cd3dd7e
--- /dev/null
+++ b/json/annotations/README.md
@@ -0,0 +1,116 @@
+# Annotations Tests Suite
+
+The Annotations Test Suite tests which annotations should appear (or not appear)
+on which values of an instance. These tests are agnostic of any output format.
+
+## Supported Dialects
+
+Although the annotation terminology of didn't appear in the spec until 2019-09,
+the concept is compatible with every version of JSON Schema. Test Cases in this
+Test Suite are designed to be compatible with as many releases of JSON Schema as
+possible. They do not include `$schema` or `$id`/`id` keywords so
+implementations can run the same Test Suite for each dialect they support.
+
+Since this Test Suite can be used for a variety of dialects, there are a couple
+of options that can be used by Test Runners to filter out Test Cases that don't
+apply to the dialect under test.
+
+## Test Case Components
+
+### description
+
+A short description of what behavior the Test Case is covering.
+
+### compatibility
+
+The `compatibility` option allows you to set which dialects the Test Case is
+compatible with. Test Runners can use this value to filter out Test Cases that
+don't apply the to dialect currently under test. The terminology for annotations
+didn't appear in the spec until 2019-09, but the concept is compatible with
+older releases as well. When setting `compatibility`, test authors should take
+into account dialects before 2019-09 for implementations that chose to support
+annotations for older dialects.
+
+Dialects are indicated by the number corresponding to their release. Date-based
+releases use just the year. If this option isn't present, it means the Test Case
+is compatible with any dialect.
+
+If this option is present with a number, the number indicates the minimum
+release the Test Case is compatible with. This example indicates that the Test
+Case is compatible with draft-07 and up.
+
+**Example**: `"compatibility": "7"`
+
+You can use a `<=` operator to indicate that the Test Case is compatible with
+releases less then or equal to the given release. This example indicates that
+the Test Case is compatible with 2019-09 and under.
+
+**Example**: `"compatibility": "<=2019"`
+
+You can use comma-separated values to indicate multiple constraints if needed.
+This example indicates that the Test Case is compatible with releases between
+draft-06 and 2019-09.
+
+**Example**: `"compatibility": "6,<=2019"`
+
+For convenience, you can use the `=` operator to indicate a Test Case is only
+compatible with a single release. This example indicates that the Test Case is
+compatible only with 2020-12.
+
+**Example**: `"compatibility": "=2020"`
+
+### schema
+
+The schema that will serve as the subject for the tests. Whenever possible, this
+schema shouldn't include `$schema` or `id`/`$id` because Test Cases should be
+designed to work with as many releases as possible.
+
+### externalSchemas
+
+This allows you to define additional schemas that `schema` makes references to.
+The value is an object where the keys are retrieval URIs and values are schemas.
+Most external schemas aren't self identifying (using `id`/`$id`) and rely on the
+retrieval URI for identification. This is done to increase the number of
+dialects that the test is compatible with. Because `id` changed to `$id` in
+draft-06, if you use `$id`, the test becomes incompatible with draft-03/4 and in
+most cases, that's not necessary.
+
+### tests
+
+A collection of Tests to run to verify the Test Case.
+
+## Test Components
+
+### instance
+
+The JSON instance to be annotated.
+
+### assertions
+
+A collection of assertions that must be true for the test to pass.
+
+## Assertions Components
+
+### location
+
+The instance location.
+
+### keyword
+
+The annotating keyword.
+
+### expected
+
+A collection of `keyword` annotations expected on the instance at `location`.
+`expected` is an object where the keys are schema locations and the values are
+the annotation that schema location contributed for the given `keyword`.
+
+There can be more than one expected annotation because multiple schema locations
+could contribute annotations for a single keyword.
+
+An empty object is an assertion that the annotation must not appear at the
+`location` for the `keyword`.
+
+As a convention for this Test Suite, the `expected` array should be sorted such
+that the most recently encountered value for an annotation given top-down
+evaluation of the schema comes before previously encountered values.
diff --git a/json/annotations/assertion.schema.json b/json/annotations/assertion.schema.json
new file mode 100644
index 000000000..882517887
--- /dev/null
+++ b/json/annotations/assertion.schema.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "type": "object",
+ "properties": {
+ "location": {
+ "markdownDescription": "The instance location.",
+ "type": "string",
+ "format": "json-pointer"
+ },
+ "keyword": {
+ "markdownDescription": "The annotation keyword.",
+ "type": "string"
+ },
+ "expected": {
+ "markdownDescription": "An object of schemaLocation/annotations pairs for `keyword` annotations expected on the instance at `location`.",
+ "type": "object",
+ "propertyNames": {
+ "format": "uri"
+ }
+ }
+ },
+ "required": ["location", "keyword", "expected"]
+}
diff --git a/json/annotations/test-case.schema.json b/json/annotations/test-case.schema.json
new file mode 100644
index 000000000..6df5f1098
--- /dev/null
+++ b/json/annotations/test-case.schema.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "type": "object",
+ "properties": {
+ "description": {
+ "markdownDescription": "A short description of what behavior the Test Case is covering.",
+ "type": "string"
+ },
+ "compatibility": {
+ "markdownDescription": "Set which dialects the Test Case is compatible with. Examples:\n- `\"7\"` -- draft-07 and above\n- `\"<=2019\"` -- 2019-09 and previous\n- `\"6,<=2019\"` -- Between draft-06 and 2019-09\n- `\"=2020\"` -- 2020-12 only",
+ "type": "string",
+ "pattern": "^(<=|=)?([123467]|2019|2020)(,(<=|=)?([123467]|2019|2020))*$"
+ },
+ "schema": {
+ "markdownDescription": "This schema shouldn't include `$schema` or `id`/`$id` unless necesary for the test because Test Cases should be designed to work with as many releases as possible.",
+ "type": ["boolean", "object"]
+ },
+ "externalSchemas": {
+ "markdownDescription": "The keys are retrieval URIs and values are schemas.",
+ "type": "object",
+ "patternProperties": {
+ "": {
+ "type": ["boolean", "object"]
+ }
+ },
+ "propertyNames": {
+ "format": "uri"
+ }
+ },
+ "tests": {
+ "markdownDescription": "A collection of Tests to run to verify the Test Case.",
+ "type": "array",
+ "items": { "$ref": "./test.schema.json" }
+ }
+ },
+ "required": ["description", "schema", "tests"]
+}
diff --git a/json/annotations/test-suite.schema.json b/json/annotations/test-suite.schema.json
new file mode 100644
index 000000000..c8b17f0d5
--- /dev/null
+++ b/json/annotations/test-suite.schema.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "suite": {
+ "type": "array",
+ "items": { "$ref": "./test-case.schema.json" }
+ }
+ },
+ "required": ["description", "suite"]
+}
diff --git a/json/annotations/test.schema.json b/json/annotations/test.schema.json
new file mode 100644
index 000000000..3581fbfca
--- /dev/null
+++ b/json/annotations/test.schema.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+
+ "type": "object",
+ "properties": {
+ "instance": {
+ "markdownDescription": "The JSON instance to be annotated."
+ },
+ "assertions": {
+ "markdownDescription": "A collection of assertions that must be true for the test to pass.",
+ "type": "array",
+ "items": { "$ref": "./assertion.schema.json" }
+ }
+ },
+ "required": ["instance", "assertions"]
+}
diff --git a/json/annotations/tests/applicators.json b/json/annotations/tests/applicators.json
new file mode 100644
index 000000000..ceb5044f3
--- /dev/null
+++ b/json/annotations/tests/applicators.json
@@ -0,0 +1,409 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The applicator vocabulary",
+ "suite": [
+ {
+ "description": "`properties`, `patternProperties`, and `additionalProperties`",
+ "compatibility": "3",
+ "schema": {
+ "properties": {
+ "foo": {
+ "title": "Foo"
+ }
+ },
+ "patternProperties": {
+ "^a": {
+ "title": "Bar"
+ }
+ },
+ "additionalProperties": {
+ "title": "Baz"
+ }
+ },
+ "tests": [
+ {
+ "instance": {},
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {}
+ },
+ {
+ "location": "/apple",
+ "keyword": "title",
+ "expected": {}
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {}
+ }
+ ]
+ },
+ {
+ "instance": {
+ "foo": {},
+ "apple": {},
+ "baz": {}
+ },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/properties/foo": "Foo"
+ }
+ },
+ {
+ "location": "/apple",
+ "keyword": "title",
+ "expected": {
+ "#/patternProperties/%5Ea": "Bar"
+ }
+ },
+ {
+ "location": "/baz",
+ "keyword": "title",
+ "expected": {
+ "#/additionalProperties": "Baz"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`propertyNames` doesn't annotate property values",
+ "compatibility": "6",
+ "schema": {
+ "propertyNames": {
+ "const": "foo",
+ "title": "Foo"
+ }
+ },
+ "tests": [
+ {
+ "instance": {
+ "foo": 42
+ },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`prefixItems` and `items`",
+ "compatibility": "2020",
+ "schema": {
+ "prefixItems": [
+ {
+ "title": "Foo"
+ }
+ ],
+ "items": {
+ "title": "Bar"
+ }
+ },
+ "tests": [
+ {
+ "instance": [
+ "foo",
+ "bar"
+ ],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/prefixItems/0": "Foo"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/items": "Bar"
+ }
+ },
+ {
+ "location": "/2",
+ "keyword": "title",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`contains`",
+ "compatibility": "6",
+ "schema": {
+ "contains": {
+ "type": "number",
+ "title": "Foo"
+ }
+ },
+ "tests": [
+ {
+ "instance": [
+ "foo",
+ 42,
+ true
+ ],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {}
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/contains": "Foo"
+ }
+ },
+ {
+ "location": "/2",
+ "keyword": "title",
+ "expected": {}
+ },
+ {
+ "location": "/3",
+ "keyword": "title",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`allOf`",
+ "compatibility": "4",
+ "schema": {
+ "allOf": [
+ {
+ "title": "Foo"
+ },
+ {
+ "title": "Bar"
+ }
+ ]
+ },
+ "tests": [
+ {
+ "instance": "foo",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/allOf/1": "Bar",
+ "#/allOf/0": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`anyOf`",
+ "compatibility": "4",
+ "schema": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "title": "Foo"
+ },
+ {
+ "type": "number",
+ "title": "Bar"
+ }
+ ]
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/anyOf/1": "Bar",
+ "#/anyOf/0": "Foo"
+ }
+ }
+ ]
+ },
+ {
+ "instance": 4.2,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/anyOf/1": "Bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`oneOf`",
+ "compatibility": "4",
+ "schema": {
+ "oneOf": [
+ {
+ "type": "string",
+ "title": "Foo"
+ },
+ {
+ "type": "number",
+ "title": "Bar"
+ }
+ ]
+ },
+ "tests": [
+ {
+ "instance": "foo",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/oneOf/0": "Foo"
+ }
+ }
+ ]
+ },
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/oneOf/1": "Bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`not`",
+ "compatibility": "4",
+ "schema": {
+ "title": "Foo",
+ "not": {
+ "not": {
+ "title": "Bar"
+ }
+ }
+ },
+ "tests": [
+ {
+ "instance": {},
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`dependentSchemas`",
+ "compatibility": "2019",
+ "schema": {
+ "dependentSchemas": {
+ "foo": {
+ "title": "Foo"
+ }
+ }
+ },
+ "tests": [
+ {
+ "instance": {
+ "foo": 42
+ },
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/dependentSchemas/foo": "Foo"
+ }
+ }
+ ]
+ },
+ {
+ "instance": {
+ "foo": 42
+ },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`if`, `then`, and `else`",
+ "compatibility": "7",
+ "schema": {
+ "if": {
+ "title": "If",
+ "type": "string"
+ },
+ "then": {
+ "title": "Then"
+ },
+ "else": {
+ "title": "Else"
+ }
+ },
+ "tests": [
+ {
+ "instance": "foo",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/then": "Then",
+ "#/if": "If"
+ }
+ }
+ ]
+ },
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/else": "Else"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/content.json b/json/annotations/tests/content.json
new file mode 100644
index 000000000..07c17a691
--- /dev/null
+++ b/json/annotations/tests/content.json
@@ -0,0 +1,121 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The content vocabulary",
+ "suite": [
+ {
+ "description": "`contentMediaType` is an annotation for string instances",
+ "compatibility": "7",
+ "schema": {
+ "contentMediaType": "application/json"
+ },
+ "tests": [
+ {
+ "instance": "{ \"foo\": \"bar\" }",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentMediaType",
+ "expected": {
+ "#": "application/json"
+ }
+ }
+ ]
+ },
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentMediaType",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`contentEncoding` is an annotation for string instances",
+ "compatibility": "7",
+ "schema": {
+ "contentEncoding": "base64"
+ },
+ "tests": [
+ {
+ "instance": "SGVsbG8gZnJvbSBKU09OIFNjaGVtYQ==",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentEncoding",
+ "expected": {
+ "#": "base64"
+ }
+ }
+ ]
+ },
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentEncoding",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`contentSchema` is an annotation for string instances",
+ "compatibility": "2019",
+ "schema": {
+ "$id": "https://annotations.json-schema.org/test/contentSchema-is-an-annotation",
+ "contentMediaType": "application/json",
+ "contentSchema": { "type": "number" }
+ },
+ "tests": [
+ {
+ "instance": "42",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentSchema",
+ "expected": {
+ "#": { "type": "number" }
+ }
+ }
+ ]
+ },
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentSchema",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`contentSchema` requires `contentMediaType`",
+ "compatibility": "2019",
+ "schema": {
+ "$id": "https://annotations.json-schema.org/test/contentSchema-is-an-annotation",
+ "contentSchema": { "type": "number" }
+ },
+ "tests": [
+ {
+ "instance": "42",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "contentSchema",
+ "expected": {}
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/core.json b/json/annotations/tests/core.json
new file mode 100644
index 000000000..1d8dee556
--- /dev/null
+++ b/json/annotations/tests/core.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The core vocabulary",
+ "suite": [
+ {
+ "description": "`$ref` and `$defs`",
+ "compatibility": "2019",
+ "schema": {
+ "$ref": "#/$defs/foo",
+ "$defs": {
+ "foo": { "title": "Foo" }
+ }
+ },
+ "tests": [
+ {
+ "instance": "foo",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#/$defs/foo": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/format.json b/json/annotations/tests/format.json
new file mode 100644
index 000000000..d8cf9a7af
--- /dev/null
+++ b/json/annotations/tests/format.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The format vocabulary",
+ "suite": [
+ {
+ "description": "`format` is an annotation",
+ "schema": {
+ "format": "email"
+ },
+ "tests": [
+ {
+ "instance": "foo@bar.com",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "format",
+ "expected": {
+ "#": "email"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/meta-data.json b/json/annotations/tests/meta-data.json
new file mode 100644
index 000000000..be99b652f
--- /dev/null
+++ b/json/annotations/tests/meta-data.json
@@ -0,0 +1,150 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The meta-data vocabulary",
+ "suite": [
+ {
+ "description": "`title` is an annotation",
+ "schema": {
+ "title": "Foo"
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "title",
+ "expected": {
+ "#": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`description` is an annotation",
+ "schema": {
+ "description": "Foo"
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "description",
+ "expected": {
+ "#": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`default` is an annotation",
+ "schema": {
+ "default": "Foo"
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "default",
+ "expected": {
+ "#": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`deprecated` is an annotation",
+ "compatibility": "2019",
+ "schema": {
+ "deprecated": true
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "deprecated",
+ "expected": {
+ "#": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`readOnly` is an annotation",
+ "compatibility": "7",
+ "schema": {
+ "readOnly": true
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "readOnly",
+ "expected": {
+ "#": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`writeOnly` is an annotation",
+ "compatibility": "7",
+ "schema": {
+ "writeOnly": true
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "writeOnly",
+ "expected": {
+ "#": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`examples` is an annotation",
+ "compatibility": "6",
+ "schema": {
+ "examples": ["Foo", "Bar"]
+ },
+ "tests": [
+ {
+ "instance": "Foo",
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "examples",
+ "expected": {
+ "#": ["Foo", "Bar"]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/unevaluated.json b/json/annotations/tests/unevaluated.json
new file mode 100644
index 000000000..9f2db1158
--- /dev/null
+++ b/json/annotations/tests/unevaluated.json
@@ -0,0 +1,661 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "The unevaluated vocabulary",
+ "suite": [
+ {
+ "description": "`unevaluatedProperties` alone",
+ "compatibility": "2019",
+ "schema": {
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `properties`",
+ "compatibility": "2019",
+ "schema": {
+ "properties": {
+ "foo": { "title": "Evaluated" }
+ },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/properties/foo": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `patternProperties`",
+ "compatibility": "2019",
+ "schema": {
+ "patternProperties": {
+ "^a": { "title": "Evaluated" }
+ },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "apple": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/apple",
+ "keyword": "title",
+ "expected": {
+ "#/patternProperties/%5Ea": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `additionalProperties`",
+ "compatibility": "2019",
+ "schema": {
+ "additionalProperties": { "title": "Evaluated" },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/additionalProperties": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/additionalProperties": "Evaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `dependentSchemas`",
+ "compatibility": "2019",
+ "schema": {
+ "dependentSchemas": {
+ "foo": {
+ "properties": {
+ "bar": { "title": "Evaluated" }
+ }
+ }
+ },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/dependentSchemas/foo/properties/bar": "Evaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `if`, `then`, and `else`",
+ "compatibility": "2019",
+ "schema": {
+ "if": {
+ "properties": {
+ "foo": {
+ "type": "string",
+ "title": "If"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "foo": { "title": "Then" }
+ }
+ },
+ "else": {
+ "properties": {
+ "foo": { "title": "Else" }
+ }
+ },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": "", "bar": 42 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/then/properties/foo": "Then",
+ "#/if/properties/foo": "If"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ },
+ {
+ "instance": { "foo": 42, "bar": "" },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/else/properties/foo": "Else"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `allOf`",
+ "compatibility": "2019",
+ "schema": {
+ "allOf": [
+ {
+ "properties": {
+ "foo": { "title": "Evaluated" }
+ }
+ }
+ ],
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/allOf/0/properties/foo": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `anyOf`",
+ "compatibility": "2019",
+ "schema": {
+ "anyOf": [
+ {
+ "properties": {
+ "foo": { "title": "Evaluated" }
+ }
+ }
+ ],
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/anyOf/0/properties/foo": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `oneOf`",
+ "compatibility": "2019",
+ "schema": {
+ "oneOf": [
+ {
+ "properties": {
+ "foo": { "title": "Evaluated" }
+ }
+ }
+ ],
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/oneOf/0/properties/foo": "Evaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedProperties` with `not`",
+ "compatibility": "2019",
+ "schema": {
+ "not": {
+ "not": {
+ "properties": {
+ "foo": { "title": "Evaluated" }
+ }
+ }
+ },
+ "unevaluatedProperties": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": { "foo": 42, "bar": 24 },
+ "assertions": [
+ {
+ "location": "/foo",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ },
+ {
+ "location": "/bar",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedProperties": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` alone",
+ "compatibility": "2019",
+ "schema": {
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `prefixItems`",
+ "compatibility": "2020",
+ "schema": {
+ "prefixItems": [{ "title": "Evaluated" }],
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/prefixItems/0": "Evaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `contains`",
+ "compatibility": "2020",
+ "schema": {
+ "contains": {
+ "type": "string",
+ "title": "Evaluated"
+ },
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": ["foo", 42],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/contains": "Evaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `if`, `then`, and `else`",
+ "compatibility": "2020",
+ "schema": {
+ "if": {
+ "prefixItems": [
+ {
+ "type": "string",
+ "title": "If"
+ }
+ ]
+ },
+ "then": {
+ "prefixItems": [
+ { "title": "Then" }
+ ]
+ },
+ "else": {
+ "prefixItems": [
+ { "title": "Else" }
+ ]
+ },
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": ["", 42],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/then/prefixItems/0": "Then",
+ "#/if/prefixItems/0": "If"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ },
+ {
+ "instance": [42, ""],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/else/prefixItems/0": "Else"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `allOf`",
+ "compatibility": "2020",
+ "schema": {
+ "allOf": [
+ {
+ "prefixItems": [
+ { "title": "Evaluated" }
+ ]
+ }
+ ],
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/allOf/0/prefixItems/0": "Evaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `anyOf`",
+ "compatibility": "2020",
+ "schema": {
+ "anyOf": [
+ {
+ "prefixItems": [
+ { "title": "Evaluated" }
+ ]
+ }
+ ],
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/anyOf/0/prefixItems/0": "Evaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `oneOf`",
+ "compatibility": "2020",
+ "schema": {
+ "oneOf": [
+ {
+ "prefixItems": [
+ { "title": "Evaluated" }
+ ]
+ }
+ ],
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/oneOf/0/prefixItems/0": "Evaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "description": "`unevaluatedItems` with `not`",
+ "compatibility": "2020",
+ "schema": {
+ "not": {
+ "not": {
+ "prefixItems": [
+ { "title": "Evaluated" }
+ ]
+ }
+ },
+ "unevaluatedItems": { "title": "Unevaluated" }
+ },
+ "tests": [
+ {
+ "instance": [42, 24],
+ "assertions": [
+ {
+ "location": "/0",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ },
+ {
+ "location": "/1",
+ "keyword": "title",
+ "expected": {
+ "#/unevaluatedItems": "Unevaluated"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/annotations/tests/unknown.json b/json/annotations/tests/unknown.json
new file mode 100644
index 000000000..b0c89003c
--- /dev/null
+++ b/json/annotations/tests/unknown.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "../test-suite.schema.json",
+ "description": "Unknown keywords",
+ "suite": [
+ {
+ "description": "`unknownKeyword` is an annotation",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "x-unknownKeyword": "Foo"
+ },
+ "tests": [
+ {
+ "instance": 42,
+ "assertions": [
+ {
+ "location": "",
+ "keyword": "x-unknownKeyword",
+ "expected": {
+ "#": "Foo"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/json/bin/annotate-specification-links b/json/bin/annotate-specification-links
new file mode 100755
index 000000000..963768b43
--- /dev/null
+++ b/json/bin/annotate-specification-links
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+"""
+Annotate pull requests to the GitHub repository with links to specifications.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+import json
+import re
+import sys
+
+from uritemplate import URITemplate
+
+
+BIN_DIR = Path(__file__).parent
+TESTS = BIN_DIR.parent / "tests"
+URLS = json.loads(BIN_DIR.joinpath("specification_urls.json").read_text())
+
+
+def urls(version: str) -> dict[str, URITemplate]:
+ """
+ Retrieve the version-specific URLs for specifications.
+ """
+ for_version = {**URLS["json-schema"][version], **URLS["external"]}
+ return {k: URITemplate(v) for k, v in for_version.items()}
+
+
+def annotation(
+ path: Path,
+ message: str,
+ line: int = 1,
+ level: str = "notice",
+ **kwargs: Any,
+) -> str:
+ """
+ Format a GitHub annotation.
+
+ See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
+ for full syntax.
+ """
+
+ if kwargs:
+ additional = "," + ",".join(f"{k}={v}" for k, v in kwargs.items())
+ else:
+ additional = ""
+
+ relative = path.relative_to(TESTS.parent)
+ return f"::{level} file={relative},line={line}{additional}::{message}\n"
+
+
+def line_number_of(path: Path, case: dict[str, Any]) -> int:
+ """
+ Crudely find the line number of a test case.
+ """
+ with path.open() as file:
+ description = case["description"]
+ return next(
+ (i + 1 for i, line in enumerate(file, 1) if description in line),
+ 1,
+ )
+
+def extract_kind_and_spec(key: str) -> (str, str):
+ """
+ Extracts specification number and kind from the defined key
+ """
+ can_have_spec = ["rfc", "iso"]
+ if not any(key.startswith(el) for el in can_have_spec):
+ return key, ""
+ number = re.search(r"\d+", key)
+ spec = "" if number is None else number.group(0)
+ kind = key.removesuffix(spec)
+ return kind, spec
+
+
+def main():
+ # Clear annotations which may have been emitted by a previous run.
+ sys.stdout.write("::remove-matcher owner=me::\n")
+
+ for version in TESTS.iterdir():
+ if version.name in {"draft-next", "latest"}:
+ continue
+
+ version_urls = urls(version.name)
+
+ for path in version.rglob("*.json"):
+ try:
+ contents = json.loads(path.read_text())
+ except json.JSONDecodeError as error:
+ error = annotation(
+ level="error",
+ path=path,
+ line=error.lineno,
+ col=error.pos + 1,
+ title=str(error),
+ message=f"cannot load {path}"
+ )
+ sys.stdout.write(error)
+ continue
+
+ for test_case in contents:
+ specifications = test_case.get("specification")
+ if specifications is not None:
+ for each in specifications:
+ quote = each.pop("quote", "")
+ (key, section), = each.items()
+
+ (kind, spec) = extract_kind_and_spec(key)
+
+ url_template = version_urls[kind]
+ if url_template is None:
+ error = annotation(
+ level="error",
+ path=path,
+ line=line_number_of(path, test_case),
+ title=f"unsupported template '{kind}'",
+ message=f"cannot find a URL template for '{kind}'"
+ )
+ sys.stdout.write(error)
+ continue
+
+ url = url_template.expand(
+ spec=spec,
+ section=section,
+ )
+
+ message = f"{url}\n\n{quote}" if quote else url
+ sys.stdout.write(
+ annotation(
+ path=path,
+ line=line_number_of(path, test_case),
+ title="Specification Link",
+ message=message,
+ ),
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/json/bin/annotation-tests.ts b/json/bin/annotation-tests.ts
new file mode 100755
index 000000000..2d3d19326
--- /dev/null
+++ b/json/bin/annotation-tests.ts
@@ -0,0 +1,31 @@
+#!/usr/bin/env deno
+import { validate } from "npm:@hyperjump/json-schema/draft-07";
+import { BASIC } from "npm:@hyperjump/json-schema/experimental";
+
+const validateTestSuite = await validate("./annotations/test-suite.schema.json");
+
+console.log("Validating annotation tests ...");
+
+let isValid = true;
+for await (const entry of Deno.readDir("./annotations/tests")) {
+ if (entry.isFile) {
+ const json = await Deno.readTextFile(`./annotations/tests/${entry.name}`);
+ const suite = JSON.parse(json);
+
+ const output = validateTestSuite(suite, BASIC);
+
+ if (output.valid) {
+ console.log(`\x1b[32m✔\x1b[0m ${entry.name}`);
+ } else {
+ isValid = false;
+ console.log(`\x1b[31m✖\x1b[0m ${entry.name}`);
+ console.log(output);
+ }
+ }
+}
+
+console.log("Done.");
+
+if (!isValid) {
+ Deno.exit(1);
+}
diff --git a/json/bin/specification_urls.json b/json/bin/specification_urls.json
new file mode 100644
index 000000000..e1824999a
--- /dev/null
+++ b/json/bin/specification_urls.json
@@ -0,0 +1,34 @@
+{
+ "json-schema": {
+ "draft2020-12": {
+ "core": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-{section}",
+ "validation": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-{section}"
+ },
+ "draft2019-09": {
+ "core": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.{section}",
+ "validation": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-validation-02#rfc.section.{section}"
+ },
+ "draft7": {
+ "core": "https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.{section}",
+ "validation": "https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.{section}"
+ },
+ "draft6": {
+ "core": "https://json-schema.org/draft-06/draft-wright-json-schema-01#rfc.section.{section}",
+ "validation": "https://json-schema.org/draft-06/draft-wright-json-schema-validation-01#rfc.section.{section}"
+ },
+ "draft4": {
+ "core": "https://json-schema.org/draft-04/draft-zyp-json-schema-04#rfc.section.{section}",
+ "validation": "https://json-schema.org/draft-04/draft-fge-json-schema-validation-00#rfc.section.{section}"
+ },
+ "draft3": {
+ "core": "https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf"
+ }
+ },
+
+ "external": {
+ "ecma262": "https://262.ecma-international.org/{section}",
+ "perl5": "https://perldoc.perl.org/perlre#{section}",
+ "rfc": "https://www.rfc-editor.org/rfc/rfc{spec}.html#section-{section}",
+ "iso": "https://www.iso.org/obp/ui"
+ }
+}
diff --git a/json/remotes/subSchemas.json b/json/remotes/draft3/subSchemas.json
similarity index 100%
rename from json/remotes/subSchemas.json
rename to json/remotes/draft3/subSchemas.json
diff --git a/json/remotes/locationIndependentIdentifierDraft4.json b/json/remotes/draft4/locationIndependentIdentifier.json
similarity index 100%
rename from json/remotes/locationIndependentIdentifierDraft4.json
rename to json/remotes/draft4/locationIndependentIdentifier.json
diff --git a/json/remotes/name.json b/json/remotes/draft4/name.json
similarity index 100%
rename from json/remotes/name.json
rename to json/remotes/draft4/name.json
diff --git a/json/remotes/draft4/subSchemas.json b/json/remotes/draft4/subSchemas.json
new file mode 100644
index 000000000..6e9b3de35
--- /dev/null
+++ b/json/remotes/draft4/subSchemas.json
@@ -0,0 +1,10 @@
+{
+ "definitions": {
+ "integer": {
+ "type": "integer"
+ },
+ "refToInteger": {
+ "$ref": "#/definitions/integer"
+ }
+ }
+}
diff --git a/json/remotes/locationIndependentIdentifierPre2019.json b/json/remotes/draft6/locationIndependentIdentifier.json
similarity index 100%
rename from json/remotes/locationIndependentIdentifierPre2019.json
rename to json/remotes/draft6/locationIndependentIdentifier.json
diff --git a/json/remotes/draft6/name.json b/json/remotes/draft6/name.json
new file mode 100644
index 000000000..fceacb809
--- /dev/null
+++ b/json/remotes/draft6/name.json
@@ -0,0 +1,15 @@
+{
+ "definitions": {
+ "orNull": {
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#"
+ }
+ ]
+ }
+ },
+ "type": "string"
+}
diff --git a/json/remotes/ref-and-definitions.json b/json/remotes/draft6/ref-and-definitions.json
similarity index 74%
rename from json/remotes/ref-and-definitions.json
rename to json/remotes/draft6/ref-and-definitions.json
index e0ee802a9..b80deeb7b 100644
--- a/json/remotes/ref-and-definitions.json
+++ b/json/remotes/draft6/ref-and-definitions.json
@@ -1,5 +1,5 @@
{
- "$id": "http://localhost:1234/ref-and-definitions.json",
+ "$id": "http://localhost:1234/draft6/ref-and-definitions.json",
"definitions": {
"inner": {
"properties": {
diff --git a/json/remotes/draft6/subSchemas.json b/json/remotes/draft6/subSchemas.json
new file mode 100644
index 000000000..6e9b3de35
--- /dev/null
+++ b/json/remotes/draft6/subSchemas.json
@@ -0,0 +1,10 @@
+{
+ "definitions": {
+ "integer": {
+ "type": "integer"
+ },
+ "refToInteger": {
+ "$ref": "#/definitions/integer"
+ }
+ }
+}
diff --git a/json/remotes/draft7/locationIndependentIdentifier.json b/json/remotes/draft7/locationIndependentIdentifier.json
new file mode 100644
index 000000000..e72815cd5
--- /dev/null
+++ b/json/remotes/draft7/locationIndependentIdentifier.json
@@ -0,0 +1,11 @@
+{
+ "definitions": {
+ "refToInteger": {
+ "$ref": "#foo"
+ },
+ "A": {
+ "$id": "#foo",
+ "type": "integer"
+ }
+ }
+}
diff --git a/json/remotes/draft7/name.json b/json/remotes/draft7/name.json
new file mode 100644
index 000000000..fceacb809
--- /dev/null
+++ b/json/remotes/draft7/name.json
@@ -0,0 +1,15 @@
+{
+ "definitions": {
+ "orNull": {
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#"
+ }
+ ]
+ }
+ },
+ "type": "string"
+}
diff --git a/json/remotes/draft7/ref-and-definitions.json b/json/remotes/draft7/ref-and-definitions.json
new file mode 100644
index 000000000..d5929380c
--- /dev/null
+++ b/json/remotes/draft7/ref-and-definitions.json
@@ -0,0 +1,11 @@
+{
+ "$id": "http://localhost:1234/draft7/ref-and-definitions.json",
+ "definitions": {
+ "inner": {
+ "properties": {
+ "bar": { "type": "string" }
+ }
+ }
+ },
+ "allOf": [ { "$ref": "#/definitions/inner" } ]
+}
diff --git a/json/remotes/draft7/subSchemas.json b/json/remotes/draft7/subSchemas.json
new file mode 100644
index 000000000..6e9b3de35
--- /dev/null
+++ b/json/remotes/draft7/subSchemas.json
@@ -0,0 +1,10 @@
+{
+ "definitions": {
+ "integer": {
+ "type": "integer"
+ },
+ "refToInteger": {
+ "$ref": "#/definitions/integer"
+ }
+ }
+}
diff --git a/json/tests/draft-next/optional/ecmascript-regex.json b/json/tests/draft-next/optional/ecmascript-regex.json
index 272114503..a1a4f9638 100644
--- a/json/tests/draft-next/optional/ecmascript-regex.json
+++ b/json/tests/draft-next/optional/ecmascript-regex.json
@@ -405,20 +405,6 @@
}
]
},
- {
- "description": "\\a is not an ECMA 262 control escape",
- "schema": {
- "$schema": "https://json-schema.org/draft/next/schema",
- "$ref": "https://json-schema.org/draft/next/schema"
- },
- "tests": [
- {
- "description": "when used as a pattern",
- "data": { "pattern": "\\a" },
- "valid": false
- }
- ]
- },
{
"description": "pattern with non-ASCII digits",
"schema": {
diff --git a/json/tests/draft-next/optional/format/duration.json b/json/tests/draft-next/optional/format/duration.json
index d5adca206..c4aa66bae 100644
--- a/json/tests/draft-next/optional/format/duration.json
+++ b/json/tests/draft-next/optional/format/duration.json
@@ -46,6 +46,11 @@
"data": "PT1D",
"valid": false
},
+ {
+ "description": "must start with P",
+ "data": "4DT12H30M5S",
+ "valid": false
+ },
{
"description": "no elements present",
"data": "P",
diff --git a/json/tests/draft-next/optional/format/ecmascript-regex.json b/json/tests/draft-next/optional/format/ecmascript-regex.json
new file mode 100644
index 000000000..1e19c2729
--- /dev/null
+++ b/json/tests/draft-next/optional/format/ecmascript-regex.json
@@ -0,0 +1,16 @@
+[
+ {
+ "description": "\\a is not an ECMA 262 control escape",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/next/schema",
+ "format": "regex"
+ },
+ "tests": [
+ {
+ "description": "when used as a pattern",
+ "data": "\\a",
+ "valid": false
+ }
+ ]
+ }
+]
diff --git a/json/tests/draft-next/optional/format/hostname.json b/json/tests/draft-next/optional/format/hostname.json
index bfb306363..bc3a60dcc 100644
--- a/json/tests/draft-next/optional/format/hostname.json
+++ b/json/tests/draft-next/optional/format/hostname.json
@@ -120,6 +120,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft-next/optional/format/idn-hostname.json b/json/tests/draft-next/optional/format/idn-hostname.json
index 109bf73c9..1061f4243 100644
--- a/json/tests/draft-next/optional/format/idn-hostname.json
+++ b/json/tests/draft-next/optional/format/idn-hostname.json
@@ -257,7 +257,7 @@
{
"description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits",
"comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8",
- "data": "\u0660\u06f0",
+ "data": "\u0628\u0660\u06f0",
"valid": false
},
{
@@ -326,6 +326,63 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ }
+ ]
+ },
+ {
+ "description": "validation of separators in internationalized host names",
+ "specification": [
+ {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"}
+ ],
+ "schema": {
+ "$schema": "https://json-schema.org/draft/next/schema",
+ "format": "idn-hostname"
+ },
+ "tests": [
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
+ },
+ {
+ "description": "single ideographic full stop",
+ "data": "\u3002",
+ "valid": false
+ },
+ {
+ "description": "single fullwidth full stop",
+ "data": "\uff0e",
+ "valid": false
+ },
+ {
+ "description": "single halfwidth ideographic full stop",
+ "data": "\uff61",
+ "valid": false
+ },
+ {
+ "description": "dot as label separator",
+ "data": "a.b",
+ "valid": true
+ },
+ {
+ "description": "ideographic full stop as label separator",
+ "data": "a\u3002b",
+ "valid": true
+ },
+ {
+ "description": "fullwidth full stop as label separator",
+ "data": "a\uff0eb",
+ "valid": true
+ },
+ {
+ "description": "halfwidth ideographic full stop as label separator",
+ "data": "a\uff61b",
+ "valid": true
}
]
}
diff --git a/json/tests/draft2019-09/optional/format/duration.json b/json/tests/draft2019-09/optional/format/duration.json
index 00d5f47ae..2d515a64a 100644
--- a/json/tests/draft2019-09/optional/format/duration.json
+++ b/json/tests/draft2019-09/optional/format/duration.json
@@ -46,6 +46,11 @@
"data": "PT1D",
"valid": false
},
+ {
+ "description": "must start with P",
+ "data": "4DT12H30M5S",
+ "valid": false
+ },
{
"description": "no elements present",
"data": "P",
diff --git a/json/tests/draft2019-09/optional/format/hostname.json b/json/tests/draft2019-09/optional/format/hostname.json
index f3b7181c8..24bfdfc5a 100644
--- a/json/tests/draft2019-09/optional/format/hostname.json
+++ b/json/tests/draft2019-09/optional/format/hostname.json
@@ -120,6 +120,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft2019-09/optional/format/idn-hostname.json b/json/tests/draft2019-09/optional/format/idn-hostname.json
index 072a6b08e..348c504c8 100644
--- a/json/tests/draft2019-09/optional/format/idn-hostname.json
+++ b/json/tests/draft2019-09/optional/format/idn-hostname.json
@@ -257,7 +257,7 @@
{
"description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits",
"comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8",
- "data": "\u0660\u06f0",
+ "data": "\u0628\u0660\u06f0",
"valid": false
},
{
@@ -326,6 +326,63 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ }
+ ]
+ },
+ {
+ "description": "validation of separators in internationalized host names",
+ "specification": [
+ {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"}
+ ],
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "format": "idn-hostname"
+ },
+ "tests": [
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
+ },
+ {
+ "description": "single ideographic full stop",
+ "data": "\u3002",
+ "valid": false
+ },
+ {
+ "description": "single fullwidth full stop",
+ "data": "\uff0e",
+ "valid": false
+ },
+ {
+ "description": "single halfwidth ideographic full stop",
+ "data": "\uff61",
+ "valid": false
+ },
+ {
+ "description": "dot as label separator",
+ "data": "a.b",
+ "valid": true
+ },
+ {
+ "description": "ideographic full stop as label separator",
+ "data": "a\u3002b",
+ "valid": true
+ },
+ {
+ "description": "fullwidth full stop as label separator",
+ "data": "a\uff0eb",
+ "valid": true
+ },
+ {
+ "description": "halfwidth ideographic full stop as label separator",
+ "data": "a\uff61b",
+ "valid": true
}
]
}
diff --git a/json/tests/draft2019-09/propertyNames.json b/json/tests/draft2019-09/propertyNames.json
index b7fecbf71..3b2bb23bb 100644
--- a/json/tests/draft2019-09/propertyNames.json
+++ b/json/tests/draft2019-09/propertyNames.json
@@ -111,5 +111,58 @@
"valid": true
}
]
+ },
+ {
+ "description": "propertyNames with const",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "propertyNames": {"const": "foo"}
+ },
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"bar": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
+ },
+ {
+ "description": "propertyNames with enum",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "propertyNames": {"enum": ["foo", "bar"]}
+ },
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with property foo and bar is valid",
+ "data": {"foo": 1, "bar": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"baz": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
}
]
diff --git a/json/tests/draft2020-12/optional/ecmascript-regex.json b/json/tests/draft2020-12/optional/ecmascript-regex.json
index 23b962e4b..a4d62e0cf 100644
--- a/json/tests/draft2020-12/optional/ecmascript-regex.json
+++ b/json/tests/draft2020-12/optional/ecmascript-regex.json
@@ -405,20 +405,6 @@
}
]
},
- {
- "description": "\\a is not an ECMA 262 control escape",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$ref": "https://json-schema.org/draft/2020-12/schema"
- },
- "tests": [
- {
- "description": "when used as a pattern",
- "data": { "pattern": "\\a" },
- "valid": false
- }
- ]
- },
{
"description": "pattern with non-ASCII digits",
"schema": {
diff --git a/json/tests/draft2020-12/optional/format/duration.json b/json/tests/draft2020-12/optional/format/duration.json
index a3af56ef0..a09fec5ef 100644
--- a/json/tests/draft2020-12/optional/format/duration.json
+++ b/json/tests/draft2020-12/optional/format/duration.json
@@ -46,6 +46,11 @@
"data": "PT1D",
"valid": false
},
+ {
+ "description": "must start with P",
+ "data": "4DT12H30M5S",
+ "valid": false
+ },
{
"description": "no elements present",
"data": "P",
diff --git a/json/tests/draft2020-12/optional/format/ecmascript-regex.json b/json/tests/draft2020-12/optional/format/ecmascript-regex.json
new file mode 100644
index 000000000..b0648084a
--- /dev/null
+++ b/json/tests/draft2020-12/optional/format/ecmascript-regex.json
@@ -0,0 +1,16 @@
+[
+ {
+ "description": "\\a is not an ECMA 262 control escape",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "format": "regex"
+ },
+ "tests": [
+ {
+ "description": "when used as a pattern",
+ "data": "\\a",
+ "valid": false
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/json/tests/draft2020-12/optional/format/hostname.json b/json/tests/draft2020-12/optional/format/hostname.json
index 41418dd4a..57827c4d4 100644
--- a/json/tests/draft2020-12/optional/format/hostname.json
+++ b/json/tests/draft2020-12/optional/format/hostname.json
@@ -120,6 +120,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft2020-12/optional/format/idn-hostname.json b/json/tests/draft2020-12/optional/format/idn-hostname.json
index bc7d92f66..f42ae969b 100644
--- a/json/tests/draft2020-12/optional/format/idn-hostname.json
+++ b/json/tests/draft2020-12/optional/format/idn-hostname.json
@@ -257,7 +257,7 @@
{
"description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits",
"comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8",
- "data": "\u0660\u06f0",
+ "data": "\u0628\u0660\u06f0",
"valid": false
},
{
@@ -326,6 +326,63 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ }
+ ]
+ },
+ {
+ "description": "validation of separators in internationalized host names",
+ "specification": [
+ {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"}
+ ],
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "format": "idn-hostname"
+ },
+ "tests": [
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
+ },
+ {
+ "description": "single ideographic full stop",
+ "data": "\u3002",
+ "valid": false
+ },
+ {
+ "description": "single fullwidth full stop",
+ "data": "\uff0e",
+ "valid": false
+ },
+ {
+ "description": "single halfwidth ideographic full stop",
+ "data": "\uff61",
+ "valid": false
+ },
+ {
+ "description": "dot as label separator",
+ "data": "a.b",
+ "valid": true
+ },
+ {
+ "description": "ideographic full stop as label separator",
+ "data": "a\u3002b",
+ "valid": true
+ },
+ {
+ "description": "fullwidth full stop as label separator",
+ "data": "a\uff0eb",
+ "valid": true
+ },
+ {
+ "description": "halfwidth ideographic full stop as label separator",
+ "data": "a\uff61b",
+ "valid": true
}
]
}
diff --git a/json/tests/draft2020-12/propertyNames.json b/json/tests/draft2020-12/propertyNames.json
index 7ecfb7ec3..b4780088a 100644
--- a/json/tests/draft2020-12/propertyNames.json
+++ b/json/tests/draft2020-12/propertyNames.json
@@ -44,6 +44,36 @@
}
]
},
+ {
+ "description": "propertyNames validation with pattern",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "propertyNames": { "pattern": "^a+$" }
+ },
+ "tests": [
+ {
+ "description": "matching property names valid",
+ "data": {
+ "a": {},
+ "aa": {},
+ "aaa": {}
+ },
+ "valid": true
+ },
+ {
+ "description": "non-matching property name is invalid",
+ "data": {
+ "aaA": {}
+ },
+ "valid": false
+ },
+ {
+ "description": "object without properties is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
+ },
{
"description": "propertyNames with boolean schema true",
"schema": {
@@ -81,5 +111,58 @@
"valid": true
}
]
+ },
+ {
+ "description": "propertyNames with const",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "propertyNames": {"const": "foo"}
+ },
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"bar": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
+ },
+ {
+ "description": "propertyNames with enum",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "propertyNames": {"enum": ["foo", "bar"]}
+ },
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with property foo and bar is valid",
+ "data": {"foo": 1, "bar": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"baz": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
}
]
diff --git a/json/tests/draft2020-12/unevaluatedProperties.json b/json/tests/draft2020-12/unevaluatedProperties.json
index ae29c9eb3..0da38f679 100644
--- a/json/tests/draft2020-12/unevaluatedProperties.json
+++ b/json/tests/draft2020-12/unevaluatedProperties.json
@@ -3,7 +3,6 @@
"description": "unevaluatedProperties true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"unevaluatedProperties": true
},
"tests": [
@@ -25,7 +24,6 @@
"description": "unevaluatedProperties schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"unevaluatedProperties": {
"type": "string",
"minLength": 3
@@ -57,7 +55,6 @@
"description": "unevaluatedProperties false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"unevaluatedProperties": false
},
"tests": [
@@ -79,7 +76,6 @@
"description": "unevaluatedProperties with adjacent properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -107,7 +103,6 @@
"description": "unevaluatedProperties with adjacent patternProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"patternProperties": {
"^foo": { "type": "string" }
},
@@ -132,13 +127,9 @@
]
},
{
- "description": "unevaluatedProperties with adjacent additionalProperties",
+ "description": "unevaluatedProperties with adjacent bool additionalProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "foo": { "type": "string" }
- },
"additionalProperties": true,
"unevaluatedProperties": false
},
@@ -160,11 +151,35 @@
}
]
},
+ {
+ "description": "unevaluatedProperties with adjacent non-bool additionalProperties",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": { "type": "string" },
+ "unevaluatedProperties": false
+ },
+ "tests": [
+ {
+ "description": "with only valid additional properties",
+ "data": {
+ "foo": "foo"
+ },
+ "valid": true
+ },
+ {
+ "description": "with invalid additional properties",
+ "data": {
+ "foo": "foo",
+ "bar": 1
+ },
+ "valid": false
+ }
+ ]
+ },
{
"description": "unevaluatedProperties with nested properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -201,7 +216,6 @@
"description": "unevaluatedProperties with nested patternProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -238,7 +252,6 @@
"description": "unevaluatedProperties with nested additionalProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -271,7 +284,6 @@
"description": "unevaluatedProperties with nested unevaluatedProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -307,7 +319,6 @@
"description": "unevaluatedProperties with anyOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -376,7 +387,6 @@
"description": "unevaluatedProperties with oneOf",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -420,7 +430,6 @@
"description": "unevaluatedProperties with not",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -449,7 +458,6 @@
"description": "unevaluatedProperties with if/then/else",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"if": {
"properties": {
"foo": { "const": "then" }
@@ -509,7 +517,6 @@
"description": "unevaluatedProperties with if/then/else, then not defined",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"if": {
"properties": {
"foo": { "const": "then" }
@@ -563,7 +570,6 @@
"description": "unevaluatedProperties with if/then/else, else not defined",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"if": {
"properties": {
"foo": { "const": "then" }
@@ -617,7 +623,6 @@
"description": "unevaluatedProperties with dependentSchemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -653,7 +658,6 @@
"description": "unevaluatedProperties with boolean schemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -681,7 +685,6 @@
"description": "unevaluatedProperties with $ref",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"$ref": "#/$defs/bar",
"properties": {
"foo": { "type": "string" }
@@ -719,7 +722,6 @@
"description": "unevaluatedProperties before $ref",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"unevaluatedProperties": false,
"properties": {
"foo": { "type": "string" }
@@ -773,7 +775,6 @@
"$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering",
"unevaluatedProperties": false,
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -862,7 +863,6 @@
"description": "nested unevaluatedProperties, outer false, inner true, properties outside",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -895,7 +895,6 @@
"description": "nested unevaluatedProperties, outer false, inner true, properties inside",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"properties": {
@@ -928,7 +927,6 @@
"description": "nested unevaluatedProperties, outer true, inner false, properties outside",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": { "type": "string" }
},
@@ -961,7 +959,6 @@
"description": "nested unevaluatedProperties, outer true, inner false, properties inside",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"properties": {
@@ -994,7 +991,6 @@
"description": "cousin unevaluatedProperties, true and false, true with properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"properties": {
@@ -1029,7 +1025,6 @@
"description": "cousin unevaluatedProperties, true and false, false with properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"unevaluatedProperties": true
@@ -1065,10 +1060,8 @@
"comment": "see https://stackoverflow.com/questions/66936884/deeply-nested-unevaluatedproperties-and-their-expectations",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"foo": {
- "type": "object",
"properties": {
"bar": {
"type": "string"
@@ -1117,7 +1110,6 @@
"description": "in-place applicator siblings, allOf has unevaluated",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"properties": {
@@ -1163,7 +1155,6 @@
"description": "in-place applicator siblings, anyOf has unevaluated",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"allOf": [
{
"properties": {
@@ -1209,7 +1200,6 @@
"description": "unevaluatedProperties + single cyclic ref",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
"properties": {
"x": { "$ref": "#" }
},
diff --git a/json/tests/draft3/optional/ecmascript-regex.json b/json/tests/draft3/optional/format/ecmascript-regex.json
similarity index 100%
rename from json/tests/draft3/optional/ecmascript-regex.json
rename to json/tests/draft3/optional/format/ecmascript-regex.json
diff --git a/json/tests/draft3/optional/format/host-name.json b/json/tests/draft3/optional/format/host-name.json
index d418f3763..9a75c3c20 100644
--- a/json/tests/draft3/optional/format/host-name.json
+++ b/json/tests/draft3/optional/format/host-name.json
@@ -57,6 +57,11 @@
"description": "exceeds maximum label length",
"data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com",
"valid": false
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
}
]
}
diff --git a/json/tests/draft3/refRemote.json b/json/tests/draft3/refRemote.json
index 0e4ab53e0..81a6c5116 100644
--- a/json/tests/draft3/refRemote.json
+++ b/json/tests/draft3/refRemote.json
@@ -17,7 +17,7 @@
},
{
"description": "fragment within remote ref",
- "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"},
+ "schema": {"$ref": "http://localhost:1234/draft3/subSchemas.json#/definitions/integer"},
"tests": [
{
"description": "remote fragment valid",
@@ -34,7 +34,7 @@
{
"description": "ref within remote ref",
"schema": {
- "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft3/subSchemas.json#/definitions/refToInteger"
},
"tests": [
{
diff --git a/json/tests/draft4/optional/format/hostname.json b/json/tests/draft4/optional/format/hostname.json
index a8ecd194f..866a61788 100644
--- a/json/tests/draft4/optional/format/hostname.json
+++ b/json/tests/draft4/optional/format/hostname.json
@@ -112,6 +112,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft4/refRemote.json b/json/tests/draft4/refRemote.json
index 64a618b89..65e45190c 100644
--- a/json/tests/draft4/refRemote.json
+++ b/json/tests/draft4/refRemote.json
@@ -17,7 +17,7 @@
},
{
"description": "fragment within remote ref",
- "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"},
+ "schema": {"$ref": "http://localhost:1234/draft4/subSchemas.json#/definitions/integer"},
"tests": [
{
"description": "remote fragment valid",
@@ -34,7 +34,7 @@
{
"description": "ref within remote ref",
"schema": {
- "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft4/subSchemas.json#/definitions/refToInteger"
},
"tests": [
{
@@ -139,7 +139,7 @@
"id": "http://localhost:1234/object",
"type": "object",
"properties": {
- "name": {"$ref": "name.json#/definitions/orNull"}
+ "name": {"$ref": "draft4/name.json#/definitions/orNull"}
}
},
"tests": [
@@ -171,7 +171,7 @@
{
"description": "Location-independent identifier in remote ref",
"schema": {
- "$ref": "http://localhost:1234/locationIndependentIdentifierDraft4.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft4/locationIndependentIdentifier.json#/definitions/refToInteger"
},
"tests": [
{
diff --git a/json/tests/draft6/optional/format/hostname.json b/json/tests/draft6/optional/format/hostname.json
index a8ecd194f..866a61788 100644
--- a/json/tests/draft6/optional/format/hostname.json
+++ b/json/tests/draft6/optional/format/hostname.json
@@ -112,6 +112,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft6/propertyNames.json b/json/tests/draft6/propertyNames.json
index f0788e649..7c7b80006 100644
--- a/json/tests/draft6/propertyNames.json
+++ b/json/tests/draft6/propertyNames.json
@@ -103,5 +103,52 @@
"valid": true
}
]
+ },
+ {
+ "description": "propertyNames with const",
+ "schema": {"propertyNames": {"const": "foo"}},
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"bar": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
+ },
+ {
+ "description": "propertyNames with enum",
+ "schema": {"propertyNames": {"enum": ["foo", "bar"]}},
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with property foo and bar is valid",
+ "data": {"foo": 1, "bar": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"baz": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
}
]
diff --git a/json/tests/draft6/refRemote.json b/json/tests/draft6/refRemote.json
index 28459c4a0..49ead6d1f 100644
--- a/json/tests/draft6/refRemote.json
+++ b/json/tests/draft6/refRemote.json
@@ -17,7 +17,7 @@
},
{
"description": "fragment within remote ref",
- "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"},
+ "schema": {"$ref": "http://localhost:1234/draft6/subSchemas.json#/definitions/integer"},
"tests": [
{
"description": "remote fragment valid",
@@ -34,7 +34,7 @@
{
"description": "ref within remote ref",
"schema": {
- "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft6/subSchemas.json#/definitions/refToInteger"
},
"tests": [
{
@@ -139,7 +139,7 @@
"$id": "http://localhost:1234/object",
"type": "object",
"properties": {
- "name": {"$ref": "name.json#/definitions/orNull"}
+ "name": {"$ref": "draft6/name.json#/definitions/orNull"}
}
},
"tests": [
@@ -173,7 +173,7 @@
"schema": {
"$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json",
"allOf": [
- { "$ref": "ref-and-definitions.json" }
+ { "$ref": "draft6/ref-and-definitions.json" }
]
},
"tests": [
@@ -196,7 +196,7 @@
{
"description": "Location-independent identifier in remote ref",
"schema": {
- "$ref": "http://localhost:1234/locationIndependentIdentifierPre2019.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft6/locationIndependentIdentifier.json#/definitions/refToInteger"
},
"tests": [
{
diff --git a/json/tests/draft7/optional/format/hostname.json b/json/tests/draft7/optional/format/hostname.json
index a8ecd194f..866a61788 100644
--- a/json/tests/draft7/optional/format/hostname.json
+++ b/json/tests/draft7/optional/format/hostname.json
@@ -112,6 +112,16 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ },
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
}
]
}
diff --git a/json/tests/draft7/optional/format/idn-hostname.json b/json/tests/draft7/optional/format/idn-hostname.json
index dc47f7b5c..5c8cdc77b 100644
--- a/json/tests/draft7/optional/format/idn-hostname.json
+++ b/json/tests/draft7/optional/format/idn-hostname.json
@@ -254,7 +254,7 @@
{
"description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits",
"comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8",
- "data": "\u0660\u06f0",
+ "data": "\u0628\u0660\u06f0",
"valid": false
},
{
@@ -318,6 +318,60 @@
"description": "single label ending with digit",
"data": "hostnam3",
"valid": true
+ },
+ {
+ "description": "empty string",
+ "data": "",
+ "valid": false
+ }
+ ]
+ },
+ {
+ "description": "validation of separators in internationalized host names",
+ "specification": [
+ {"rfc3490": "3.1", "quote": "Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61(halfwidth ideographic full stop)"}
+ ],
+ "schema": { "format": "idn-hostname" },
+ "tests": [
+ {
+ "description": "single dot",
+ "data": ".",
+ "valid": false
+ },
+ {
+ "description": "single ideographic full stop",
+ "data": "\u3002",
+ "valid": false
+ },
+ {
+ "description": "single fullwidth full stop",
+ "data": "\uff0e",
+ "valid": false
+ },
+ {
+ "description": "single halfwidth ideographic full stop",
+ "data": "\uff61",
+ "valid": false
+ },
+ {
+ "description": "dot as label separator",
+ "data": "a.b",
+ "valid": true
+ },
+ {
+ "description": "ideographic full stop as label separator",
+ "data": "a\u3002b",
+ "valid": true
+ },
+ {
+ "description": "fullwidth full stop as label separator",
+ "data": "a\uff0eb",
+ "valid": true
+ },
+ {
+ "description": "halfwidth ideographic full stop as label separator",
+ "data": "a\uff61b",
+ "valid": true
}
]
}
diff --git a/json/tests/draft7/propertyNames.json b/json/tests/draft7/propertyNames.json
index f0788e649..7c7b80006 100644
--- a/json/tests/draft7/propertyNames.json
+++ b/json/tests/draft7/propertyNames.json
@@ -103,5 +103,52 @@
"valid": true
}
]
+ },
+ {
+ "description": "propertyNames with const",
+ "schema": {"propertyNames": {"const": "foo"}},
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"bar": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
+ },
+ {
+ "description": "propertyNames with enum",
+ "schema": {"propertyNames": {"enum": ["foo", "bar"]}},
+ "tests": [
+ {
+ "description": "object with property foo is valid",
+ "data": {"foo": 1},
+ "valid": true
+ },
+ {
+ "description": "object with property foo and bar is valid",
+ "data": {"foo": 1, "bar": 1},
+ "valid": true
+ },
+ {
+ "description": "object with any other property is invalid",
+ "data": {"baz": 1},
+ "valid": false
+ },
+ {
+ "description": "empty object is valid",
+ "data": {},
+ "valid": true
+ }
+ ]
}
]
diff --git a/json/tests/draft7/refRemote.json b/json/tests/draft7/refRemote.json
index 22185d678..450787af6 100644
--- a/json/tests/draft7/refRemote.json
+++ b/json/tests/draft7/refRemote.json
@@ -17,7 +17,7 @@
},
{
"description": "fragment within remote ref",
- "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"},
+ "schema": {"$ref": "http://localhost:1234/draft7/subSchemas.json#/definitions/integer"},
"tests": [
{
"description": "remote fragment valid",
@@ -34,7 +34,7 @@
{
"description": "ref within remote ref",
"schema": {
- "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft7/subSchemas.json#/definitions/refToInteger"
},
"tests": [
{
@@ -139,7 +139,7 @@
"$id": "http://localhost:1234/object",
"type": "object",
"properties": {
- "name": {"$ref": "name.json#/definitions/orNull"}
+ "name": {"$ref": "draft7/name.json#/definitions/orNull"}
}
},
"tests": [
@@ -173,7 +173,7 @@
"schema": {
"$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json",
"allOf": [
- { "$ref": "ref-and-definitions.json" }
+ { "$ref": "draft7/ref-and-definitions.json" }
]
},
"tests": [
@@ -196,7 +196,7 @@
{
"description": "Location-independent identifier in remote ref",
"schema": {
- "$ref": "http://localhost:1234/locationIndependentIdentifierPre2019.json#/definitions/refToInteger"
+ "$ref": "http://localhost:1234/draft7/locationIndependentIdentifier.json#/definitions/refToInteger"
},
"tests": [
{
diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py
index 79924cf7e..d8dec8cfa 100644
--- a/jsonschema/__init__.py
+++ b/jsonschema/__init__.py
@@ -106,12 +106,12 @@ def __getattr__(name):
__all__ = [
- "Draft201909Validator",
- "Draft202012Validator",
"Draft3Validator",
"Draft4Validator",
"Draft6Validator",
"Draft7Validator",
+ "Draft201909Validator",
+ "Draft202012Validator",
"FormatChecker",
"SchemaError",
"TypeChecker",
diff --git a/jsonschema/_format.py b/jsonschema/_format.py
index 6e87620cc..9b4e67b63 100644
--- a/jsonschema/_format.py
+++ b/jsonschema/_format.py
@@ -13,9 +13,7 @@
_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], ...],
-]
+_RaisesType = typing.Union[type[Exception], tuple[type[Exception], ...]]
_RE_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII)
@@ -274,6 +272,10 @@ def is_ipv6(instance: object) -> bool:
draft7="hostname",
draft201909="hostname",
draft202012="hostname",
+ # fqdn.FQDN("") raises a ValueError due to a bug
+ # however, it's not clear when or if that will be fixed, so catch it
+ # here for now
+ raises=ValueError,
)
def is_host_name(instance: object) -> bool:
if not isinstance(instance, str):
diff --git a/jsonschema/_types.py b/jsonschema/_types.py
index bf25e7e6f..d3ce9d667 100644
--- a/jsonschema/_types.py
+++ b/jsonschema/_types.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any, Callable, Mapping
+from typing import TYPE_CHECKING
import numbers
from attrs import evolve, field, frozen
@@ -8,6 +8,10 @@
from jsonschema.exceptions import UndefinedTypeCheck
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+ from typing import Any, Callable
+
# unfortunately, the type of HashTrieMap is generic, and if used as an attrs
# converter, the generic type is presented to mypy, which then fails to match
@@ -192,7 +196,7 @@ def remove(self, *types) -> TypeChecker:
"integer",
lambda checker, instance: (
is_integer(checker, instance)
- or isinstance(instance, float) and instance.is_integer()
+ or (isinstance(instance, float) and instance.is_integer())
),
)
draft7_type_checker = draft6_type_checker
diff --git a/jsonschema/_typing.py b/jsonschema/_typing.py
index d283dc48d..1d091d70c 100644
--- a/jsonschema/_typing.py
+++ b/jsonschema/_typing.py
@@ -1,7 +1,8 @@
"""
Some (initially private) typing helpers for jsonschema's types.
"""
-from typing import Any, Callable, Iterable, Protocol, Tuple, Union
+from collections.abc import Iterable
+from typing import Any, Callable, Protocol, Union
import referencing.jsonschema
@@ -24,5 +25,5 @@ def __call__(
ApplicableValidators = Callable[
[referencing.jsonschema.Schema],
- Iterable[Tuple[str, Any]],
+ Iterable[tuple[str, Any]],
]
diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py
index 54d28c041..84a0965e5 100644
--- a/jsonschema/_utils.py
+++ b/jsonschema/_utils.py
@@ -298,18 +298,18 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
),
)
- for keyword in [
- "properties", "additionalProperties", "unevaluatedProperties",
- ]:
- if keyword in schema:
- schema_value = schema[keyword]
- if validator.is_type(schema_value, "boolean") and schema_value:
- evaluated_keys += instance.keys()
-
- elif validator.is_type(schema_value, "object"):
- for property in schema_value:
- if property in instance:
- evaluated_keys.append(property)
+ properties = schema.get("properties")
+ if validator.is_type(properties, "object"):
+ evaluated_keys += properties.keys() & instance.keys()
+
+ for keyword in ["additionalProperties", "unevaluatedProperties"]:
+ if (subschema := schema.get(keyword)) is None:
+ continue
+ evaluated_keys += (
+ key
+ for key, value in instance.items()
+ if is_valid(validator.descend(value, subschema))
+ )
if "patternProperties" in schema:
for property in instance:
@@ -326,13 +326,12 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
)
for keyword in ["allOf", "oneOf", "anyOf"]:
- if keyword in schema:
- for subschema in schema[keyword]:
- errs = next(validator.descend(instance, subschema), None)
- if errs is None:
- evaluated_keys += find_evaluated_property_keys_by_schema(
- validator, instance, subschema,
- )
+ for subschema in schema.get(keyword, []):
+ if not is_valid(validator.descend(instance, subschema)):
+ continue
+ evaluated_keys += find_evaluated_property_keys_by_schema(
+ validator, instance, subschema,
+ )
if "if" in schema:
if validator.evolve(schema=schema["if"]).is_valid(instance):
@@ -349,3 +348,8 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema):
)
return evaluated_keys
+
+
+def is_valid(errs_it):
+ """Whether there are no errors in the given iterator."""
+ return next(errs_it, None) is None
diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py
index 78da49fcd..3dcd29667 100644
--- a/jsonschema/exceptions.py
+++ b/jsonschema/exceptions.py
@@ -8,7 +8,6 @@
from textwrap import dedent, indent
from typing import TYPE_CHECKING, Any, ClassVar
import heapq
-import itertools
import warnings
from attrs import define
@@ -471,11 +470,9 @@ def best_match(errors, key=relevance):
set of inputs from version to version if better heuristics are added.
"""
- errors = iter(errors)
- best = next(errors, None)
+ best = max(errors, key=key, default=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
diff --git a/jsonschema/protocols.py b/jsonschema/protocols.py
index 39e56d0fa..0fd993eec 100644
--- a/jsonschema/protocols.py
+++ b/jsonschema/protocols.py
@@ -7,21 +7,14 @@
from __future__ import annotations
-from typing import (
- TYPE_CHECKING,
- Any,
- ClassVar,
- Iterable,
- Protocol,
- runtime_checkable,
-)
+from typing import TYPE_CHECKING, Any, ClassVar, 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 collections.abc import Mapping
+ from collections.abc import Iterable, Mapping
import referencing.jsonschema
diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py
index 0da6503c1..d61d38277 100644
--- a/jsonschema/tests/_suite.py
+++ b/jsonschema/tests/_suite.py
@@ -10,7 +10,6 @@
import json
import os
import re
-import subprocess
import sys
import unittest
@@ -21,11 +20,14 @@
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
+ from referencing.jsonschema import Schema
import pyperf
from jsonschema.validators import _VALIDATORS
import jsonschema
+MAGIC_REMOTE_URL = "http://localhost:1234"
+
_DELIMITERS = re.compile(r"[\W\- ]+")
@@ -51,38 +53,7 @@ def _find_suite():
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():
@@ -92,10 +63,18 @@ def benchmark(self, runner: pyperf.Runner): # pragma: no cover
)
def version(self, name) -> Version:
+ Validator = _VALIDATORS[name]
+ uri: str = Validator.ID_OF(Validator.META_SCHEMA) # type: ignore[assignment]
+ specification = referencing.jsonschema.specification_with(uri)
+
+ registry = Registry().with_contents(
+ remotes_in(root=self._root / "remotes", name=name, uri=uri),
+ default_specification=specification,
+ )
return Version(
name=name,
path=self._root / "tests" / name,
- remotes=self._remotes,
+ remotes=registry,
)
@@ -187,6 +166,36 @@ def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover
)
+def remotes_in(
+ root: Path,
+ name: str,
+ uri: str,
+) -> Iterable[tuple[str, Schema]]:
+ # This messy logic is because the test suite is terrible at indicating
+ # what remotes are needed for what drafts, and mixes in schemas which
+ # have no $schema and which are invalid under earlier versions, in with
+ # other schemas which are needed for tests.
+
+ for each in root.rglob("*.json"):
+ schema = json.loads(each.read_text())
+
+ relative = str(each.relative_to(root)).replace("\\", "/")
+
+ if (
+ ( # invalid boolean schema
+ name in {"draft3", "draft4"}
+ and each.stem == "tree"
+ ) or
+ ( # draft/*.json
+ "$schema" not in schema
+ and relative.startswith("draft")
+ and not relative.startswith(name)
+ )
+ ):
+ continue
+ yield f"{MAGIC_REMOTE_URL}/{relative}", schema
+
+
@frozen(repr=False)
class _Test:
diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py
index 79d2a1584..bed9f3e4c 100644
--- a/jsonschema/tests/test_cli.py
+++ b/jsonschema/tests/test_cli.py
@@ -690,7 +690,7 @@ def test_successful_validation_of_just_the_schema_pretty_output(self):
)
def test_successful_validation_via_explicit_base_uri(self):
- ref_schema_file = tempfile.NamedTemporaryFile(delete=False)
+ ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
ref_schema_file.close()
self.addCleanup(os.remove, ref_schema_file.name)
@@ -711,7 +711,7 @@ def test_successful_validation_via_explicit_base_uri(self):
)
def test_unsuccessful_validation_via_explicit_base_uri(self):
- ref_schema_file = tempfile.NamedTemporaryFile(delete=False)
+ ref_schema_file = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
ref_schema_file.close()
self.addCleanup(os.remove, ref_schema_file.name)
@@ -881,11 +881,8 @@ def test_useless_error_format(self):
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)
+ our_metadata = metadata.metadata("jsonschema")
+ self.assertEqual(our_metadata.get("License-Expression"), "MIT")
def test_version(self):
version = subprocess.check_output(
diff --git a/jsonschema/tests/test_deprecations.py b/jsonschema/tests/test_deprecations.py
index aea922d23..a54b02f38 100644
--- a/jsonschema/tests/test_deprecations.py
+++ b/jsonschema/tests/test_deprecations.py
@@ -183,7 +183,7 @@ def test_RefResolver(self):
self.assertEqual(w.filename, __file__)
with self.assertWarnsRegex(DeprecationWarning, message) as w:
- from jsonschema.validators import RefResolver # noqa: F401, F811
+ from jsonschema.validators import RefResolver # noqa: F401
self.assertEqual(w.filename, __file__)
def test_RefResolutionError(self):
diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py
index 282c1369c..41c982553 100644
--- a/jsonschema/tests/test_jsonschema_test_suite.py
+++ b/jsonschema/tests/test_jsonschema_test_suite.py
@@ -6,7 +6,6 @@
See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details.
"""
-import sys
from jsonschema.tests._suite import Suite
import jsonschema
@@ -27,6 +26,11 @@ def skipper(test):
return skipper
+def ecmascript_regex(test):
+ if test.subject == "ecmascript-regex":
+ return "ECMA regex support will be added in #1142."
+
+
def missing_format(Validator):
def missing_format(test): # pragma: no cover
schema = test.schema
@@ -66,18 +70,6 @@ def complex_email_validation(test):
)(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(
@@ -132,7 +124,8 @@ def leap_second(test):
Validator=jsonschema.Draft3Validator,
format_checker=jsonschema.Draft3Validator.FORMAT_CHECKER,
skip=lambda test: (
- missing_format(jsonschema.Draft3Validator)(test)
+ ecmascript_regex(test)
+ or missing_format(jsonschema.Draft3Validator)(test)
or complex_email_validation(test)
),
)
@@ -149,7 +142,7 @@ def leap_second(test):
Validator=jsonschema.Draft4Validator,
format_checker=jsonschema.Draft4Validator.FORMAT_CHECKER,
skip=lambda test: (
- allowed_leading_zeros(test)
+ ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft4Validator)(test)
or complex_email_validation(test)
@@ -167,7 +160,7 @@ def leap_second(test):
Validator=jsonschema.Draft6Validator,
format_checker=jsonschema.Draft6Validator.FORMAT_CHECKER,
skip=lambda test: (
- allowed_leading_zeros(test)
+ ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft6Validator)(test)
or complex_email_validation(test)
@@ -187,7 +180,7 @@ def leap_second(test):
Validator=jsonschema.Draft7Validator,
format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER,
skip=lambda test: (
- allowed_leading_zeros(test)
+ ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft7Validator)(test)
or complex_email_validation(test)
@@ -224,7 +217,7 @@ def leap_second(test):
format_checker=jsonschema.Draft201909Validator.FORMAT_CHECKER,
skip=lambda test: (
complex_email_validation(test)
- or allowed_leading_zeros(test)
+ or ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft201909Validator)(test)
or complex_email_validation(test)
@@ -261,7 +254,7 @@ def leap_second(test):
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
skip=lambda test: (
complex_email_validation(test)
- or allowed_leading_zeros(test)
+ or ecmascript_regex(test)
or leap_second(test)
or missing_format(jsonschema.Draft202012Validator)(test)
or complex_email_validation(test)
diff --git a/jsonschema/validators.py b/jsonschema/validators.py
index 85c39160d..b8ca3bd45 100644
--- a/jsonschema/validators.py
+++ b/jsonschema/validators.py
@@ -857,7 +857,7 @@ def extend(
version="draft2020-12",
)
-_LATEST_VERSION = Draft202012Validator
+_LATEST_VERSION: type[Validator] = Draft202012Validator
class _RefResolver:
@@ -1334,7 +1334,7 @@ def validate(instance, schema, cls=None, *args, **kwargs): # noqa: D417
def validator_for(
schema,
- default: Validator | _utils.Unset = _UNSET,
+ default: type[Validator] | _utils.Unset = _UNSET,
) -> type[Validator]:
"""
Retrieve the validator class appropriate for validating the given schema.
@@ -1396,7 +1396,7 @@ class is returned:
DefaultValidator = _LATEST_VERSION if default is _UNSET else default
if schema is True or schema is False or "$schema" not in schema:
- return DefaultValidator
+ return DefaultValidator # type: ignore[return-value]
if schema["$schema"] not in _META_SCHEMAS and default is _UNSET:
warn(
(
diff --git a/noxfile.py b/noxfile.py
index fada3e1f8..2f750385c 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -22,22 +22,24 @@
docs=DOCS / "requirements.txt",
)
REQUIREMENTS_IN = [ # this is actually ordered, as files depend on each other
- path.parent / f"{path.stem}.in" for path in REQUIREMENTS.values()
+ (path.parent / f"{path.stem}.in", path) for path in REQUIREMENTS.values()
]
NONGPL_LICENSES = [
"Apache Software License",
"BSD License",
"ISC License (ISCL)",
+ "MIT",
"MIT License",
"Mozilla Public License 2.0 (MPL 2.0)",
"Python Software Foundation License",
"The Unlicense (Unlicense)",
]
-SUPPORTED = ["3.8", "3.9", "3.10", "pypy3.10", "3.11", "3.12", "3.13"]
-LATEST_STABLE = "3.12"
+SUPPORTED = ["3.9", "3.10", "pypy3.11", "3.11", "3.12", "3.13"]
+LATEST_STABLE = SUPPORTED[-1]
+nox.options.default_venv_backend = "uv|virtualenv"
nox.options.sessions = []
@@ -115,9 +117,19 @@ def license_check(session):
"-m",
"piplicenses",
"--ignore-packages",
+
+ # because they're not our deps
"pip-requirements-parser",
"pip_audit",
"pip-api",
+
+ # because pip-licenses doesn't yet support PEP 639 :/
+ "attrs",
+ "jsonschema",
+ "jsonschema-specifications",
+ "referencing",
+ "types-python-dateutil",
+
"--allow-only",
";".join(NONGPL_LICENSES),
)
@@ -128,9 +140,15 @@ def build(session):
"""
Build a distribution suitable for PyPI and check its validity.
"""
- session.install("build", "docutils", "twine")
+ session.install("build[uv]", "docutils", "twine")
with TemporaryDirectory() as tmpdir:
- session.run("python", "-m", "build", ROOT, "--outdir", tmpdir)
+ session.run(
+ "pyproject-build",
+ "--installer=uv",
+ ROOT,
+ "--outdir",
+ tmpdir,
+ )
session.run("twine", "check", "--strict", tmpdir + "/*")
session.run(
"python", "-m", "docutils", "--strict", CHANGELOG, os.devnull,
@@ -143,7 +161,7 @@ def secrets(session):
Check for accidentally committed secrets.
"""
session.install("detect-secrets")
- session.run("detect-secrets", "scan", ROOT)
+ session.run("detect-secrets", "scan", ROOT, "--exclude-files", "json/")
@session(tags=["style"])
@@ -239,13 +257,13 @@ def requirements(session):
You should commit the result afterwards.
"""
- session.install("pip-tools")
- for each in REQUIREMENTS_IN:
- session.run(
- "pip-compile",
- "--resolver",
- "backtracking",
- "--strip-extras",
- "-U",
- each.relative_to(ROOT),
- )
+ if session.venv_backend == "uv":
+ cmd = ["uv", "pip", "compile"]
+ else:
+ session.install("pip-tools")
+ cmd = ["pip-compile", "--resolver", "backtracking", "--strip-extras"]
+
+ for each, out in REQUIREMENTS_IN:
+ # otherwise output files end up with silly absolute path comments...
+ relative = each.relative_to(ROOT)
+ session.run(*cmd, "--upgrade", "--output-file", out, relative)
diff --git a/pyproject.toml b/pyproject.toml
index 1eea228f3..3c7c40c2a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,8 +8,9 @@ source = "vcs"
[project]
name = "jsonschema"
description = "An implementation of JSON Schema validation for Python"
-requires-python = ">=3.8"
-license = {text = "MIT"}
+requires-python = ">=3.9"
+license = "MIT"
+license-files = ["COPYING"]
keywords = [
"validation",
"data validation",
@@ -23,10 +24,8 @@ authors = [
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",
@@ -130,17 +129,10 @@ skip_covered = true
[tool.doc8]
ignore = [
- "D000", # see PyCQA/doc8#125
- "D001", # one sentence per line, so max length doesn't make sense
+ "D000", # see PyCQA/doc8#125
+ "D001", # one sentence per line, so max length doesn't make sense
]
-[tool.isort]
-combine_as_imports = true
-ensure_newline_before_comments = true
-from_first = true
-include_trailing_comma = true
-multi_line_output = 3
-
[tool.mypy]
ignore_missing_imports = true
show_error_codes = true
@@ -156,6 +148,7 @@ ignore = [
"A001", # It's fine to shadow builtins
"A002",
"A003",
+ "A005",
"ARG", # This is all wrong whenever an interface is involved
"ANN", # Just let the type checker do this
"B006", # Mutable arguments require care but are OK if you don't abuse them
@@ -185,6 +178,7 @@ ignore = [
"PLR0913",
"PLR0915",
"PLR1714", # This makes for uglier comparisons sometimes
+ "PLW0642", # Shadowing self also isn't a big deal.
"PLW2901", # Shadowing for loop variables is occasionally fine.
"PT", # We use unittest
"PYI025", # wat, I'm not confused, thanks.
@@ -197,7 +191,7 @@ ignore = [
"SLF001", # Private usage within this package itself is fine
"TD", # These TODO style rules are also silly
"TRY003", # Some exception classes are essentially intended for free-form
- "UP007", # We support 3.8 + 3.9
+ "UP007", # We support 3.9
]
[tool.ruff.lint.flake8-pytest-style]
]