json-schema-engine 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. json_schema_engine-0.0.2/CHANGELOG.md +47 -0
  2. json_schema_engine-0.0.2/PKG-INFO +314 -0
  3. json_schema_engine-0.0.2/README.md +287 -0
  4. json_schema_engine-0.0.2/pyproject.toml +175 -0
  5. json_schema_engine-0.0.2/src/json_schema_engine/compiler/__init__.py +113 -0
  6. json_schema_engine-0.0.2/src/json_schema_engine/compiler/emit.py +255 -0
  7. json_schema_engine-0.0.2/src/json_schema_engine/compiler/errors.py +16 -0
  8. json_schema_engine-0.0.2/src/json_schema_engine/compiler/plan.py +415 -0
  9. json_schema_engine-0.0.2/src/json_schema_engine/compiler/py.typed +0 -0
  10. json_schema_engine-0.0.2/src/json_schema_engine/compiler/runtime.py +114 -0
  11. json_schema_engine-0.0.2/src/json_schema_engine/compiler/runtime_compile.py +29 -0
  12. json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/__init__.py +170 -0
  13. json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/body.py +524 -0
  14. json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/context.py +155 -0
  15. json_schema_engine-0.0.2/src/json_schema_engine/compiler/serialize/units.py +181 -0
  16. json_schema_engine-0.0.2/src/json_schema_engine/compiler/standalone.py +151 -0
  17. json_schema_engine-0.0.2/src/json_schema_engine/core/__init__.py +158 -0
  18. json_schema_engine-0.0.2/src/json_schema_engine/core/channel.py +172 -0
  19. json_schema_engine-0.0.2/src/json_schema_engine/core/cursor.py +78 -0
  20. json_schema_engine-0.0.2/src/json_schema_engine/core/dialect.py +494 -0
  21. json_schema_engine-0.0.2/src/json_schema_engine/core/engine.py +484 -0
  22. json_schema_engine-0.0.2/src/json_schema_engine/core/errors.py +191 -0
  23. json_schema_engine-0.0.2/src/json_schema_engine/core/evaluator.py +613 -0
  24. json_schema_engine-0.0.2/src/json_schema_engine/core/formats.py +50 -0
  25. json_schema_engine-0.0.2/src/json_schema_engine/core/json_model.py +245 -0
  26. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/__init__.py +1 -0
  27. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/_ids.py +54 -0
  28. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/applicator.py +353 -0
  29. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/applicator_array.py +312 -0
  30. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/applicator_object.py +323 -0
  31. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/content.py +58 -0
  32. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/core.py +180 -0
  33. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/dialect2019.py +163 -0
  34. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/dialect2020.py +88 -0
  35. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/dialect7.py +189 -0
  36. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/dialects.py +30 -0
  37. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/format.py +146 -0
  38. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/legacy.py +348 -0
  39. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/meta_data.py +20 -0
  40. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/unevaluated.py +266 -0
  41. json_schema_engine-0.0.2/src/json_schema_engine/core/keywords/validation.py +642 -0
  42. json_schema_engine-0.0.2/src/json_schema_engine/core/loader.py +96 -0
  43. json_schema_engine-0.0.2/src/json_schema_engine/core/lowering.py +463 -0
  44. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/applicator.json +53 -0
  45. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/content.json +14 -0
  46. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/core.json +54 -0
  47. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/format.json +11 -0
  48. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/meta-data.json +34 -0
  49. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/schema.json +42 -0
  50. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2019-09/validation.json +95 -0
  51. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/applicator.json +45 -0
  52. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/content.json +14 -0
  53. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/core.json +48 -0
  54. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/format-annotation.json +11 -0
  55. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/format-assertion.json +11 -0
  56. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/meta-data.json +34 -0
  57. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/schema.json +58 -0
  58. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/unevaluated.json +12 -0
  59. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/2020-12/validation.json +95 -0
  60. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/__init__.py +61 -0
  61. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/draft-06/schema.json +155 -0
  62. json_schema_engine-0.0.2/src/json_schema_engine/core/metaschemas/draft-07/schema.json +172 -0
  63. json_schema_engine-0.0.2/src/json_schema_engine/core/output.py +640 -0
  64. json_schema_engine-0.0.2/src/json_schema_engine/core/positions.py +287 -0
  65. json_schema_engine-0.0.2/src/json_schema_engine/core/py.typed +0 -0
  66. json_schema_engine-0.0.2/src/json_schema_engine/core/records.py +189 -0
  67. json_schema_engine-0.0.2/src/json_schema_engine/core/ref.py +35 -0
  68. json_schema_engine-0.0.2/src/json_schema_engine/core/regex.py +114 -0
  69. json_schema_engine-0.0.2/src/json_schema_engine/core/registry.py +484 -0
  70. json_schema_engine-0.0.2/src/json_schema_engine/core/result.py +258 -0
  71. json_schema_engine-0.0.2/src/json_schema_engine/core/uri.py +204 -0
  72. json_schema_engine-0.0.2/src/json_schema_engine/formats/__init__.py +136 -0
  73. json_schema_engine-0.0.2/src/json_schema_engine/formats/_abnf.py +70 -0
  74. json_schema_engine-0.0.2/src/json_schema_engine/formats/datetime_.py +161 -0
  75. json_schema_engine-0.0.2/src/json_schema_engine/formats/idna_.py +105 -0
  76. json_schema_engine-0.0.2/src/json_schema_engine/formats/misc.py +62 -0
  77. json_schema_engine-0.0.2/src/json_schema_engine/formats/net.py +165 -0
  78. json_schema_engine-0.0.2/src/json_schema_engine/formats/pointer.py +57 -0
  79. json_schema_engine-0.0.2/src/json_schema_engine/formats/py.typed +0 -0
  80. json_schema_engine-0.0.2/src/json_schema_engine/formats/uri.py +129 -0
  81. json_schema_engine-0.0.1/PKG-INFO +0 -48
  82. json_schema_engine-0.0.1/README.md +0 -26
  83. json_schema_engine-0.0.1/pyproject.toml +0 -50
  84. json_schema_engine-0.0.1/src/json_schema_engine/core/__init__.py +0 -7
  85. json_schema_engine-0.0.1/tests/test_import.py +0 -12
  86. {json_schema_engine-0.0.1 → json_schema_engine-0.0.2}/.gitignore +0 -0
  87. {json_schema_engine-0.0.1 → json_schema_engine-0.0.2}/LICENSE +0 -0
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to `json-schema-engine` (the Python engine). The
4
+ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
+ versions follow [SemVer](https://semver.org/) with the 0.x caveat that
6
+ minor versions may change public API.
7
+
8
+ ## [0.0.2] - unreleased
9
+
10
+ The first functional release. Everything below is new relative to the
11
+ 0.0.1 name reservation.
12
+
13
+ ### Added
14
+
15
+ - `json_schema_engine.core`: the interpreter tier. Draft 2020-12 (default),
16
+ 2019-09, draft-07, and draft-06 with each draft's own semantics
17
+ (`$dynamicRef`, `$recursiveRef`, `$ref` sibling handling); `$vocabulary`
18
+ processing with the bundled metaschemas; loaders for remote references
19
+ with source-position reporting (`parse_json_with_ranges`,
20
+ `positions=True`, `Engine.locate`); metaschema validation on request
21
+ (`validate_schemas=True`); the `flag`, `basic`, `detailed`, `verbose`,
22
+ `list`, and `hierarchical` output formats with annotation selection,
23
+ error params, verbose levels, and the application trace; ECMA-262
24
+ regular expressions through `ecma-regex`, with a Python `re`
25
+ passthrough dialect and the optional `regex` backend; the
26
+ `reject_unsafe_regex` screen, `max_depth`, and O(n) `uniqueItems`.
27
+ - `json_schema_engine.compiler`: `compile_validator` (a verdict-only
28
+ Python function bound to a registry snapshot, trampolining into the
29
+ interpreter for whatever it cannot lower), `emit_standalone` (a module
30
+ importable without the compiler), and `explain_compilation`.
31
+ - `json_schema_engine.formats`: every format the four drafts define,
32
+ implemented from the RFCs and verified against the official
33
+ `optional/format` suites; the `idna` extra for IDNA2008
34
+ (`idn-hostname`, A-label checks in `hostname`); assertion opt-in through
35
+ `create_engine(formats=..., assert_formats=...)` and the 2020-12
36
+ format-assertion vocabulary.
37
+ - Every distribution portion ships a `py.typed` marker.
38
+
39
+ ### Changed
40
+
41
+ - The distribution depends on `ecma-regex>=0.1,<0.2`.
42
+ - The sdist no longer carries the test tree, which needs the repository's
43
+ test-kit and the suite submodule.
44
+
45
+ ## [0.0.1] - 2026-09-20
46
+
47
+ Name reservation on PyPI; no functionality.
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.5
2
+ Name: json-schema-engine
3
+ Version: 0.0.2
4
+ Summary: A spec-complete, annotation-first JSON Schema engine for Python: interpreter, compiler, formats, every standard output format.
5
+ Project-URL: Homepage, https://github.com/handrews/py-json-schema-engine
6
+ Project-URL: Repository, https://github.com/handrews/py-json-schema-engine
7
+ Project-URL: Issues, https://github.com/handrews/py-json-schema-engine/issues
8
+ Author: Henry Andrews
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: json,json-schema,validation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: ecma-regex<0.2,>=0.1
22
+ Provides-Extra: idna
23
+ Requires-Dist: idna>=3.7; extra == 'idna'
24
+ Provides-Extra: regex
25
+ Requires-Dist: regex; extra == 'regex'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # json-schema-engine (Python)
29
+
30
+ A JSON Schema implementation for Python that aims to be both spec-complete
31
+ and built for speed: an interpreter that is the reference semantics, a
32
+ compiler tier for hot paths, full annotation collection, and every standard
33
+ output format. It is the Python counterpart of
34
+ [handrews/json-schema-engine](https://github.com/handrews/json-schema-engine)
35
+ and shares its architecture: two tiers, one keyword registry, a frame-scoped
36
+ record channel, and the new (2026)
37
+ [IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
38
+ relevance model.
39
+
40
+ Produced by Henry Andrews via Claude Code.
41
+
42
+ **Status: `0.0.2` is the first functional release.**
43
+
44
+ All `0.0.x` releases will have AI-written documentation. Version
45
+ `0.1.0` will indicate that the documentation has been audited and revised
46
+ by a human.
47
+
48
+ As of `0.0.2`, this package carries the
49
+ interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
50
+ standard output format, the compiler tier (a flag validator and standalone
51
+ modules, M6–M7), and the `json_schema_engine.formats` package — green on
52
+ every official test-suite file for those drafts through both tiers, on the
53
+ official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
54
+ contract and carries the milestone status.
55
+
56
+ The regular-expression translator lives in its own package,
57
+ [`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
58
+ for Python, with JavaScript semantics, no dependency on this engine.
59
+
60
+ ## Install
61
+
62
+ ```sh
63
+ pip install json-schema-engine
64
+ ```
65
+
66
+ Python 3.12+. Extras: `[idna]` for IDNA2008 support (`idn-hostname`, and the
67
+ `xn--` label check inside `hostname`); `[regex]` to swap in the `regex`
68
+ package as the pattern backend.
69
+
70
+ ## Use
71
+
72
+ ```python
73
+ from json_schema_engine.core import create_engine
74
+
75
+ engine = create_engine()
76
+ uri = engine.register_schema(
77
+ {
78
+ "type": "object",
79
+ "properties": {"name": {"type": "string"}},
80
+ "required": ["name"],
81
+ },
82
+ "https://example.com/person",
83
+ )
84
+ assert engine.evaluate(uri, {"name": "Ada"}).valid
85
+ result = engine.evaluate(uri, {}, output="list")
86
+ assert result.errors is not None and result.errors[0]["evaluationPath"] == "/required"
87
+ ```
88
+
89
+ Four drafts are built in: 2020-12 (the default), 2019-09, draft-07, and
90
+ draft-06. A document's `$schema` selects its dialect; `create_engine(
91
+ default_dialect=DIALECT_DRAFT_07)` sets the dialect for documents without
92
+ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
93
+ siblings while a 2019-09 one does not.
94
+
95
+ Every standard output format is available by name: `flag` (the default),
96
+ `basic`, `detailed`, and `verbose` from the IETF draft-03 output spec, and
97
+ `list` and `hierarchical` from the machines-oriented proposal. Annotations
98
+ are a separate control (`annotations=True`, or an `AnnotationSelection`),
99
+ `verbose=True` asks `list`/`hierarchical` for the verbose level with
100
+ irrelevant records marked as dropped, and `trace=True` adds the application
101
+ tree with error indexes into `result.errors`.
102
+
103
+ ```python
104
+ result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
105
+ assert result.output_document == {
106
+ "valid": False,
107
+ "evaluationPath": "",
108
+ "schemaLocation": "https://example.com/person#",
109
+ "instanceLocation": "",
110
+ "details": [
111
+ {
112
+ "valid": False,
113
+ "evaluationPath": "/properties/name",
114
+ "schemaLocation": "https://example.com/person#/properties/name",
115
+ "instanceLocation": "/name",
116
+ "errors": {"type": "expected string"},
117
+ }
118
+ ],
119
+ }
120
+ ```
121
+
122
+ `create_engine(validate_schemas=True)` checks every registered document
123
+ against its metaschema and raises `SchemaValidationError` with the errors.
124
+ A loader that reports source positions (see `parse_json_with_ranges`,
125
+ exported from `json_schema_engine.core`) lets `evaluate(..., positions=True)`
126
+ attach a `source` location to every error and annotation, and
127
+ `engine.locate()` answers the same question for any schema location.
128
+
129
+ ## Compile
130
+
131
+ The compiler tier turns a registered schema into a Python function. It is
132
+ not a second implementation: any subschema it cannot emit (a `$dynamicRef`,
133
+ an `unevaluated*` whose coverage is only known at runtime, an in-place
134
+ cycle) calls back into the interpreter, so a compiled validator is exactly
135
+ as correct as `Engine.evaluate` and never less complete. Tier choice is a
136
+ performance decision, not a semantic one.
137
+
138
+ ```python
139
+ from json_schema_engine.compiler import compile_validator, emit_standalone
140
+
141
+ compiled = compile_validator(engine, uri)
142
+ assert compiled.validate({"name": "Ada"}) is True
143
+ assert compiled.validate({}) is False
144
+ print(compiled.source) # the emitted module, for reading
145
+ module_source = emit_standalone(engine, uri) # importable without the compiler
146
+ assert "def validate" in module_source
147
+ assert "import json_schema_engine.compiler" not in module_source
148
+ ```
149
+
150
+ `compile_validator` returns a verdict-only validator (the flag level) that
151
+ binds a snapshot of the registries at compile time, so register everything
152
+ first. `emit_standalone` writes the same code as a module that imports
153
+ only the standard library and this package's pure helpers; it refuses,
154
+ with `StandaloneUnsupportedError`, a schema that would need the
155
+ interpreter at evaluation time. Compiled code assumes plain data as
156
+ `json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
157
+ `None`); subclasses of those types belong to the interpreter. Errors,
158
+ annotations, and the output formats are interpreter features today;
159
+ compiled output beyond the verdict is a later milestone.
160
+
161
+ ## Formats
162
+
163
+ `format` annotates by default in every dialect (the specs' default, and
164
+ what the official `format.json` legs require). Assertion is opt-in, from a
165
+ format table implemented from each format's RFC and verified against the
166
+ official `optional/format` suite:
167
+
168
+ ```python
169
+ from json_schema_engine.core import create_engine
170
+ from json_schema_engine.formats import FORMATS_2020_12, format_table_for
171
+
172
+ # The 2020-12 format-assertion vocabulary: a metaschema declaring it makes
173
+ # `format` assert; names the table lacks are refused at registration.
174
+ engine = create_engine(formats=FORMATS_2020_12)
175
+ # Best effort in every standard dialect: known names assert, unknown names
176
+ # annotate only.
177
+ engine = create_engine(formats=FORMATS_2020_12, assert_formats=True)
178
+ uri = engine.register_schema({"format": "date-time"}, "https://example.com/dt")
179
+ assert engine.evaluate(uri, "1998-12-31T23:59:60Z").valid
180
+ assert not engine.evaluate(uri, "1998-12-31T22:59:60Z").valid
181
+ ```
182
+
183
+ `FORMATS_2020_12` (also 2019-09) carries the nineteen defined formats;
184
+ `FORMATS_DRAFT_07` and `FORMATS_DRAFT_06` carry each draft's list, and
185
+ `format_table_for(dialect_uri)` picks one. A metaschema that declares the
186
+ format-assertion vocabulary on an engine without a table raises
187
+ `FormatsRequiredError`, and `assert_formats=True` without a table does too.
188
+ A custom table is any mapping of names to `FormatDefinition(test, types)`;
189
+ `types` scopes a format to instance types other than strings.
190
+
191
+ `idn-hostname` and the A-label checks inside `hostname` need IDNA2008,
192
+ provided by the `idna` extra:
193
+
194
+ ```sh
195
+ pip install 'json-schema-engine[idna]'
196
+ ```
197
+
198
+ Without it, asserting `idn-hostname` raises `FormatUnavailableError` at
199
+ registration, and `hostname` accepts a well-formed `xn--` label without
200
+ decoding it. Compiled validators assert formats too; a standalone module
201
+ imports the predicates it needs from `json_schema_engine.formats`, so the
202
+ extra must be installed wherever such a module runs.
203
+
204
+ ## Documentation
205
+
206
+ - [User guide](docs/guide/index.md) — validation, output formats,
207
+ annotations, dialects, loaders, source positions, metaschemas, custom
208
+ keywords, security, compiling schemas, and formats, topic by topic.
209
+ - [API reference](docs/reference.md) — every public name, by package.
210
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, gates, and conventions.
211
+ - [CHANGELOG.md](CHANGELOG.md) — what changed, release by release.
212
+ - [DESIGN.md](DESIGN.md) — the engineering design contract and milestone
213
+ status.
214
+
215
+ ## Security
216
+
217
+ Schemas and instances are both often untrusted input. The interpreter
218
+ generates no code — there is no `compile()` or `exec()` on its path — so
219
+ code-injection concerns do not apply to it; a denial-of-service bound is
220
+ best effort, not a guarantee, so treat wildly untrusted schemas with the
221
+ same care as any other untrusted program input. Three specific vectors have
222
+ a bound or an opt-out.
223
+
224
+ **Regular expressions (ReDoS).** `pattern` and `patternProperties` compile
225
+ untrusted regexes and run them against untrusted strings; Python's `re` can
226
+ backtrack catastrophically on a pattern like `(a+)+$`. `reject_unsafe_regex`
227
+ screens for nested unbounded quantifiers at registration:
228
+
229
+ ```python
230
+ from json_schema_engine.core import create_engine, UnsafeRegexError
231
+
232
+ engine = create_engine(reject_unsafe_regex=True)
233
+ try:
234
+ engine.register_schema({"pattern": "(a+)+$"}, "https://ex/redos")
235
+ except UnsafeRegexError:
236
+ pass # rejected before it ever runs
237
+ else:
238
+ raise AssertionError("expected UnsafeRegexError")
239
+ ```
240
+
241
+ Patterns are ECMA-262 by default, translated by the `ecma-regex` package to
242
+ the `re` backend (a `regex` backend is available via the `regex` extra);
243
+ neither backend is linear-time, so the screen is a heuristic, not a proof.
244
+ `regex_dialect="python"` hands patterns to `re` untouched, for schemas
245
+ written for Python only.
246
+
247
+ **Recursion depth.** `max_depth` (default 512) bounds both registration
248
+ nesting and evaluation nesting, raising the typed `MaxDepthExceededError`
249
+ before CPython's own stack limit can produce an untyped `RecursionError`; a
250
+ stray `RecursionError` that does slip through is still converted to the
251
+ same typed error. The engine stays usable afterward — each `evaluate` or
252
+ `register_schema` call runs in fresh state, so a rejected document does not
253
+ poison later calls.
254
+
255
+ **Array uniqueness.** `uniqueItems` compares elements in O(n) by bucketing
256
+ on a canonical key and confirming collisions with full JSON equality, so
257
+ large arrays of distinct values do not incur quadratic cost, while genuine
258
+ duplicates — including numbers equal across `int`/`float` and objects that
259
+ differ only in member order — are still reported.
260
+
261
+ **No prototype hazard.** Python dicts have no prototype chain, so there is
262
+ nothing for a hostile property name to pollute: `__proto__`, `constructor`,
263
+ and similar reserved-looking names evaluate as ordinary properties.
264
+
265
+ ## Development
266
+
267
+ ```sh
268
+ uv sync --all-packages --all-groups
269
+ uv run pytest -q
270
+ uv run ruff check . && uv run ruff format --check .
271
+ uv run pyright
272
+ uv run lint-imports
273
+ ```
274
+
275
+ The official test suite is a git submodule at `test-suite/`; clone with
276
+ `--recurse-submodules` or run `git submodule update --init`.
277
+
278
+ The [Bowtie](https://bowtie.report) conformance leg builds a container
279
+ image and runs the suite through Bowtie's harness protocol. It needs a
280
+ reachable container engine (Docker, or `podman machine start`) and fetches
281
+ Bowtie through `uvx`:
282
+
283
+ ```sh
284
+ uv run python scripts/bowtie_check.py
285
+ ```
286
+
287
+ ### Benchmarks
288
+
289
+ ```sh
290
+ uv run python scripts/bench.py --budget-ms 250 --filter user
291
+ ```
292
+
293
+ `scripts/bench.py` times the compiler tier's flag and standalone artifacts
294
+ against the interpreter and two competitors (fastjsonschema, jsonschema)
295
+ over seven corpora in `packages/bench`: three small hand-authored schemas,
296
+ the official OpenAPI 3.1 schema against a real document, a generated
297
+ API-payload corpus, and two 2000-record corpora. It is report-only (it
298
+ enforces no performance threshold) and, per the IP policy below, runs the
299
+ competitors only, never reading or porting their source. `--filter` takes
300
+ a regex over corpus/subject/partition names; omit `--out` to skip writing
301
+ JSON; `--compare BEFORE AFTER` prints a before/after comparison of two
302
+ results files. The committed run lives at
303
+ `packages/bench/results/results.json` (`--budget-ms 250`). The interpreter
304
+ is the reference semantics, so ratios are informational, not a
305
+ compatibility claim. [`packages/bench/README.md`](packages/bench/README.md)
306
+ covers corpus provenance and licensing, methodology, and the recorded
307
+ exclusions.
308
+
309
+ ## IP policy
310
+
311
+ The implementation is written from the JSON Schema specifications and the
312
+ official test suite only. Other validators are executed as benchmark subjects
313
+ and correctness oracles; their source is never used as an implementation
314
+ reference. See [DESIGN.md](DESIGN.md) §0.
@@ -0,0 +1,287 @@
1
+ # json-schema-engine (Python)
2
+
3
+ A JSON Schema implementation for Python that aims to be both spec-complete
4
+ and built for speed: an interpreter that is the reference semantics, a
5
+ compiler tier for hot paths, full annotation collection, and every standard
6
+ output format. It is the Python counterpart of
7
+ [handrews/json-schema-engine](https://github.com/handrews/json-schema-engine)
8
+ and shares its architecture: two tiers, one keyword registry, a frame-scoped
9
+ record channel, and the new (2026)
10
+ [IETF working group draft-03](https://www.ietf.org/archive/id/draft-ietf-jsonschema-json-schema-03.html)
11
+ relevance model.
12
+
13
+ Produced by Henry Andrews via Claude Code.
14
+
15
+ **Status: `0.0.2` is the first functional release.**
16
+
17
+ All `0.0.x` releases will have AI-written documentation. Version
18
+ `0.1.0` will indicate that the documentation has been audited and revised
19
+ by a human.
20
+
21
+ As of `0.0.2`, this package carries the
22
+ interpreter core for 2020-12, 2019-09, draft-07, and draft-06 with every
23
+ standard output format, the compiler tier (a flag validator and standalone
24
+ modules, M6–M7), and the `json_schema_engine.formats` package — green on
25
+ every official test-suite file for those drafts through both tiers, on the
26
+ official output-tests, and on Bowtie. [DESIGN.md](DESIGN.md) is the design
27
+ contract and carries the milestone status.
28
+
29
+ The regular-expression translator lives in its own package,
30
+ [`ecma-regex`](packages/ecma-regex/README.md) (`0.1.0`): ECMA-262 patterns
31
+ for Python, with JavaScript semantics, no dependency on this engine.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ pip install json-schema-engine
37
+ ```
38
+
39
+ Python 3.12+. Extras: `[idna]` for IDNA2008 support (`idn-hostname`, and the
40
+ `xn--` label check inside `hostname`); `[regex]` to swap in the `regex`
41
+ package as the pattern backend.
42
+
43
+ ## Use
44
+
45
+ ```python
46
+ from json_schema_engine.core import create_engine
47
+
48
+ engine = create_engine()
49
+ uri = engine.register_schema(
50
+ {
51
+ "type": "object",
52
+ "properties": {"name": {"type": "string"}},
53
+ "required": ["name"],
54
+ },
55
+ "https://example.com/person",
56
+ )
57
+ assert engine.evaluate(uri, {"name": "Ada"}).valid
58
+ result = engine.evaluate(uri, {}, output="list")
59
+ assert result.errors is not None and result.errors[0]["evaluationPath"] == "/required"
60
+ ```
61
+
62
+ Four drafts are built in: 2020-12 (the default), 2019-09, draft-07, and
63
+ draft-06. A document's `$schema` selects its dialect; `create_engine(
64
+ default_dialect=DIALECT_DRAFT_07)` sets the dialect for documents without
65
+ one. Each keeps its own semantics, so a draft-07 `$ref` ignores its
66
+ siblings while a 2019-09 one does not.
67
+
68
+ Every standard output format is available by name: `flag` (the default),
69
+ `basic`, `detailed`, and `verbose` from the IETF draft-03 output spec, and
70
+ `list` and `hierarchical` from the machines-oriented proposal. Annotations
71
+ are a separate control (`annotations=True`, or an `AnnotationSelection`),
72
+ `verbose=True` asks `list`/`hierarchical` for the verbose level with
73
+ irrelevant records marked as dropped, and `trace=True` adds the application
74
+ tree with error indexes into `result.errors`.
75
+
76
+ ```python
77
+ result = engine.evaluate(uri, {"name": 3}, output="hierarchical")
78
+ assert result.output_document == {
79
+ "valid": False,
80
+ "evaluationPath": "",
81
+ "schemaLocation": "https://example.com/person#",
82
+ "instanceLocation": "",
83
+ "details": [
84
+ {
85
+ "valid": False,
86
+ "evaluationPath": "/properties/name",
87
+ "schemaLocation": "https://example.com/person#/properties/name",
88
+ "instanceLocation": "/name",
89
+ "errors": {"type": "expected string"},
90
+ }
91
+ ],
92
+ }
93
+ ```
94
+
95
+ `create_engine(validate_schemas=True)` checks every registered document
96
+ against its metaschema and raises `SchemaValidationError` with the errors.
97
+ A loader that reports source positions (see `parse_json_with_ranges`,
98
+ exported from `json_schema_engine.core`) lets `evaluate(..., positions=True)`
99
+ attach a `source` location to every error and annotation, and
100
+ `engine.locate()` answers the same question for any schema location.
101
+
102
+ ## Compile
103
+
104
+ The compiler tier turns a registered schema into a Python function. It is
105
+ not a second implementation: any subschema it cannot emit (a `$dynamicRef`,
106
+ an `unevaluated*` whose coverage is only known at runtime, an in-place
107
+ cycle) calls back into the interpreter, so a compiled validator is exactly
108
+ as correct as `Engine.evaluate` and never less complete. Tier choice is a
109
+ performance decision, not a semantic one.
110
+
111
+ ```python
112
+ from json_schema_engine.compiler import compile_validator, emit_standalone
113
+
114
+ compiled = compile_validator(engine, uri)
115
+ assert compiled.validate({"name": "Ada"}) is True
116
+ assert compiled.validate({}) is False
117
+ print(compiled.source) # the emitted module, for reading
118
+ module_source = emit_standalone(engine, uri) # importable without the compiler
119
+ assert "def validate" in module_source
120
+ assert "import json_schema_engine.compiler" not in module_source
121
+ ```
122
+
123
+ `compile_validator` returns a verdict-only validator (the flag level) that
124
+ binds a snapshot of the registries at compile time, so register everything
125
+ first. `emit_standalone` writes the same code as a module that imports
126
+ only the standard library and this package's pure helpers; it refuses,
127
+ with `StandaloneUnsupportedError`, a schema that would need the
128
+ interpreter at evaluation time. Compiled code assumes plain data as
129
+ `json.loads` produces it (`dict`, `list`, `str`, `int`, `float`, `bool`,
130
+ `None`); subclasses of those types belong to the interpreter. Errors,
131
+ annotations, and the output formats are interpreter features today;
132
+ compiled output beyond the verdict is a later milestone.
133
+
134
+ ## Formats
135
+
136
+ `format` annotates by default in every dialect (the specs' default, and
137
+ what the official `format.json` legs require). Assertion is opt-in, from a
138
+ format table implemented from each format's RFC and verified against the
139
+ official `optional/format` suite:
140
+
141
+ ```python
142
+ from json_schema_engine.core import create_engine
143
+ from json_schema_engine.formats import FORMATS_2020_12, format_table_for
144
+
145
+ # The 2020-12 format-assertion vocabulary: a metaschema declaring it makes
146
+ # `format` assert; names the table lacks are refused at registration.
147
+ engine = create_engine(formats=FORMATS_2020_12)
148
+ # Best effort in every standard dialect: known names assert, unknown names
149
+ # annotate only.
150
+ engine = create_engine(formats=FORMATS_2020_12, assert_formats=True)
151
+ uri = engine.register_schema({"format": "date-time"}, "https://example.com/dt")
152
+ assert engine.evaluate(uri, "1998-12-31T23:59:60Z").valid
153
+ assert not engine.evaluate(uri, "1998-12-31T22:59:60Z").valid
154
+ ```
155
+
156
+ `FORMATS_2020_12` (also 2019-09) carries the nineteen defined formats;
157
+ `FORMATS_DRAFT_07` and `FORMATS_DRAFT_06` carry each draft's list, and
158
+ `format_table_for(dialect_uri)` picks one. A metaschema that declares the
159
+ format-assertion vocabulary on an engine without a table raises
160
+ `FormatsRequiredError`, and `assert_formats=True` without a table does too.
161
+ A custom table is any mapping of names to `FormatDefinition(test, types)`;
162
+ `types` scopes a format to instance types other than strings.
163
+
164
+ `idn-hostname` and the A-label checks inside `hostname` need IDNA2008,
165
+ provided by the `idna` extra:
166
+
167
+ ```sh
168
+ pip install 'json-schema-engine[idna]'
169
+ ```
170
+
171
+ Without it, asserting `idn-hostname` raises `FormatUnavailableError` at
172
+ registration, and `hostname` accepts a well-formed `xn--` label without
173
+ decoding it. Compiled validators assert formats too; a standalone module
174
+ imports the predicates it needs from `json_schema_engine.formats`, so the
175
+ extra must be installed wherever such a module runs.
176
+
177
+ ## Documentation
178
+
179
+ - [User guide](docs/guide/index.md) — validation, output formats,
180
+ annotations, dialects, loaders, source positions, metaschemas, custom
181
+ keywords, security, compiling schemas, and formats, topic by topic.
182
+ - [API reference](docs/reference.md) — every public name, by package.
183
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, gates, and conventions.
184
+ - [CHANGELOG.md](CHANGELOG.md) — what changed, release by release.
185
+ - [DESIGN.md](DESIGN.md) — the engineering design contract and milestone
186
+ status.
187
+
188
+ ## Security
189
+
190
+ Schemas and instances are both often untrusted input. The interpreter
191
+ generates no code — there is no `compile()` or `exec()` on its path — so
192
+ code-injection concerns do not apply to it; a denial-of-service bound is
193
+ best effort, not a guarantee, so treat wildly untrusted schemas with the
194
+ same care as any other untrusted program input. Three specific vectors have
195
+ a bound or an opt-out.
196
+
197
+ **Regular expressions (ReDoS).** `pattern` and `patternProperties` compile
198
+ untrusted regexes and run them against untrusted strings; Python's `re` can
199
+ backtrack catastrophically on a pattern like `(a+)+$`. `reject_unsafe_regex`
200
+ screens for nested unbounded quantifiers at registration:
201
+
202
+ ```python
203
+ from json_schema_engine.core import create_engine, UnsafeRegexError
204
+
205
+ engine = create_engine(reject_unsafe_regex=True)
206
+ try:
207
+ engine.register_schema({"pattern": "(a+)+$"}, "https://ex/redos")
208
+ except UnsafeRegexError:
209
+ pass # rejected before it ever runs
210
+ else:
211
+ raise AssertionError("expected UnsafeRegexError")
212
+ ```
213
+
214
+ Patterns are ECMA-262 by default, translated by the `ecma-regex` package to
215
+ the `re` backend (a `regex` backend is available via the `regex` extra);
216
+ neither backend is linear-time, so the screen is a heuristic, not a proof.
217
+ `regex_dialect="python"` hands patterns to `re` untouched, for schemas
218
+ written for Python only.
219
+
220
+ **Recursion depth.** `max_depth` (default 512) bounds both registration
221
+ nesting and evaluation nesting, raising the typed `MaxDepthExceededError`
222
+ before CPython's own stack limit can produce an untyped `RecursionError`; a
223
+ stray `RecursionError` that does slip through is still converted to the
224
+ same typed error. The engine stays usable afterward — each `evaluate` or
225
+ `register_schema` call runs in fresh state, so a rejected document does not
226
+ poison later calls.
227
+
228
+ **Array uniqueness.** `uniqueItems` compares elements in O(n) by bucketing
229
+ on a canonical key and confirming collisions with full JSON equality, so
230
+ large arrays of distinct values do not incur quadratic cost, while genuine
231
+ duplicates — including numbers equal across `int`/`float` and objects that
232
+ differ only in member order — are still reported.
233
+
234
+ **No prototype hazard.** Python dicts have no prototype chain, so there is
235
+ nothing for a hostile property name to pollute: `__proto__`, `constructor`,
236
+ and similar reserved-looking names evaluate as ordinary properties.
237
+
238
+ ## Development
239
+
240
+ ```sh
241
+ uv sync --all-packages --all-groups
242
+ uv run pytest -q
243
+ uv run ruff check . && uv run ruff format --check .
244
+ uv run pyright
245
+ uv run lint-imports
246
+ ```
247
+
248
+ The official test suite is a git submodule at `test-suite/`; clone with
249
+ `--recurse-submodules` or run `git submodule update --init`.
250
+
251
+ The [Bowtie](https://bowtie.report) conformance leg builds a container
252
+ image and runs the suite through Bowtie's harness protocol. It needs a
253
+ reachable container engine (Docker, or `podman machine start`) and fetches
254
+ Bowtie through `uvx`:
255
+
256
+ ```sh
257
+ uv run python scripts/bowtie_check.py
258
+ ```
259
+
260
+ ### Benchmarks
261
+
262
+ ```sh
263
+ uv run python scripts/bench.py --budget-ms 250 --filter user
264
+ ```
265
+
266
+ `scripts/bench.py` times the compiler tier's flag and standalone artifacts
267
+ against the interpreter and two competitors (fastjsonschema, jsonschema)
268
+ over seven corpora in `packages/bench`: three small hand-authored schemas,
269
+ the official OpenAPI 3.1 schema against a real document, a generated
270
+ API-payload corpus, and two 2000-record corpora. It is report-only (it
271
+ enforces no performance threshold) and, per the IP policy below, runs the
272
+ competitors only, never reading or porting their source. `--filter` takes
273
+ a regex over corpus/subject/partition names; omit `--out` to skip writing
274
+ JSON; `--compare BEFORE AFTER` prints a before/after comparison of two
275
+ results files. The committed run lives at
276
+ `packages/bench/results/results.json` (`--budget-ms 250`). The interpreter
277
+ is the reference semantics, so ratios are informational, not a
278
+ compatibility claim. [`packages/bench/README.md`](packages/bench/README.md)
279
+ covers corpus provenance and licensing, methodology, and the recorded
280
+ exclusions.
281
+
282
+ ## IP policy
283
+
284
+ The implementation is written from the JSON Schema specifications and the
285
+ official test suite only. Other validators are executed as benchmark subjects
286
+ and correctness oracles; their source is never used as an implementation
287
+ reference. See [DESIGN.md](DESIGN.md) §0.