json1-7 Exercise 1: What Calling a Validator Costs ==================================================== Run with Node 24.18.0 (Ajv 8.20.0, ajv-formats 3.0.1, ajv-errors 3.0.0, json-schema-to-typescript 16.0.0, TypeScript 7.0.2, vscode-json-languageservice 6.0.0-next.4) and Python 3.14.0 (jsonschema 4.26.0, json-schema-for-humans 1.5.1), installed in a scratch folder. Timings are single runs on one Windows 11 machine and will differ on yours; compare the RATIOS. (The numbers quoted in the chapter come from an earlier run on the same machine: for example 9.7 ms per document for compile-each-time there, 8.4 ms here; the ratios are about the same.) --- t1.mjs (Ajv) --- import { createRequire } from 'node:module'; import fs from 'node:fs'; const require = createRequire(import.meta.url); const Ajv2020 = require('ajv/dist/2020'); const addFormats = require('ajv-formats'); const schema = JSON.parse(fs.readFileSync('order.schema.json', 'utf8')); const orders = JSON.parse(fs.readFileSync('orders.json', 'utf8')); const N = 20000, docs = Array.from({ length: N }, (_, i) => orders[i % orders.length]); const ms = f => { const t = process.hrtime.bigint(); f(); return Number(process.hrtime.bigint() - t) / 1e6; }; // 1. compile once, validate N times let validate; const tCompile = ms(() => { const a = new Ajv2020(); addFormats(a); validate = a.compile(schema); }); let ok1 = 0; const tOnce = ms(() => { for (const d of docs) if (validate(d)) ok1++; }); // 2. compile a new Ajv + schema for every document (the mistake), on fewer docs const M = 200; let ok2 = 0; const tEach = ms(() => { for (let i = 0; i < M; i++) { const a = new Ajv2020(); addFormats(a); if (a.compile(schema)(docs[i])) ok2++; } }); // 3. ajv.validate(schema, data) with the same Ajv and same schema object (cached) const a3 = new Ajv2020(); addFormats(a3); let ok3 = 0; const tCached = ms(() => { for (const d of docs) if (a3.validate(schema, d)) ok3++; }); // 4. ajv.validate with a freshly parsed copy of the schema each call (cache miss), same Ajv const a4 = new Ajv2020(); addFormats(a4); let ok4 = 0, err4 = ''; const tFresh = ms(() => { try { for (let i = 0; i < 200; i++) if (a4.validate(JSON.parse(JSON.stringify(schema)), docs[i])) ok4++; } catch (e) { err4 = e.message; } }); console.log('compile once: ' + tCompile.toFixed(1) + ' ms to compile'); console.log(' then ' + N + ' validations: ' + tOnce.toFixed(1) + ' ms (' + (tOnce / N * 1000).toFixed(2) + ' microseconds each), valid ' + ok1 + '/' + N); console.log('new Ajv + compile per document: ' + tEach.toFixed(1) + ' ms for ' + M + ' documents (' + (tEach / M).toFixed(2) + ' ms each), valid ' + ok2 + '/' + M); console.log('ajv.validate(schemaObject, doc) x ' + N + ': ' + tCached.toFixed(1) + ' ms (same object: cached), valid ' + ok3 + '/' + N); console.log('ajv.validate(fresh copy of schema, doc) x 200: ' + tFresh.toFixed(1) + ' ms, valid ' + ok4 + '/200 ' + (err4 ? 'ERROR: ' + err4 : '')); Output: compile once: 44.4 ms to compile then 20000 validations: 8.0 ms (0.40 microseconds each), valid 20000/20000 new Ajv + compile per document: 1680.0 ms for 200 documents (8.40 ms each), valid 200/200 ajv.validate(schemaObject, doc) x 20000: 14.5 ms (same object: cached), valid 20000/20000 ajv.validate(fresh copy of schema, doc) x 200: 9.8 ms, valid 1/200 ERROR: schema with key or id "https://example.com/schemas/order.json" already exists --- t2.py (jsonschema) --- import json, time import jsonschema from jsonschema import Draft202012Validator as V, FormatChecker schema = json.load(open('order.schema.json', encoding='utf-8')) orders = json.load(open('orders.json', encoding='utf-8')) N = 5000; docs = [orders[i % len(orders)] for i in range(N)] def timed(f): t = time.perf_counter(); r = f(); return (time.perf_counter() - t) * 1000, r t_build, v = timed(lambda: V(schema, format_checker=FormatChecker())) t_reuse, ok = timed(lambda: sum(1 for d in docs if v.is_valid(d))) t_validate_fn, ok2 = timed(lambda: sum(1 for d in docs[:500] if (jsonschema.validate(d, schema) is None))) t_new_each, ok3 = timed(lambda: sum(1 for d in docs[:500] if V(schema).is_valid(d))) t_check_each, _ = timed(lambda: [V.check_schema(schema) for _ in range(500)]) print('build the validator once: %7.1f ms' % t_build) print(' then %d is_valid calls: %7.1f ms (%.1f microseconds each), valid %d/%d' % (N, t_reuse, t_reuse / N * 1000, ok, N)) print('jsonschema.validate(doc, schema) x 500: %7.1f ms (%.2f ms each)' % (t_validate_fn, t_validate_fn / 500)) print('V(schema).is_valid(doc) x 500: %7.1f ms (%.2f ms each)' % (t_new_each, t_new_each / 500)) print('V.check_schema(schema) alone x 500: %7.1f ms (%.2f ms each)' % (t_check_each, t_check_each / 500)) Output: build the validator once: 0.0 ms then 5000 is_valid calls: 308.1 ms (61.6 microseconds each), valid 5000/5000 jsonschema.validate(doc, schema) x 500: 1199.2 ms (2.40 ms each) V(schema).is_valid(doc) x 500: 31.5 ms (0.06 ms each) V.check_schema(schema) alone x 500: 1116.5 ms (2.23 ms each) --- t6.py (its first lines show validate() against is_valid() on a broken schema; the rest is Exercise 3 material) --- import json from jsonschema import Draft202012Validator as V from jsonschema.exceptions import best_match, ValidationError events = { 'click': {'type': 'object', 'properties': {'type': {'const': 'click'}, 'x': {'type': 'integer'}, 'y': {'type': 'integer'}}, 'required': ['type', 'x', 'y']}, 'key': {'type': 'object', 'properties': {'type': {'const': 'key'}, 'key': {'type': 'string', 'minLength': 1}}, 'required': ['type', 'key']}, 'scroll':{'type': 'object', 'properties': {'type': {'const': 'scroll'}, 'dy': {'type': 'number'}}, 'required': ['type', 'dy']}, } bad = {'type': 'click', 'x': 'ten', 'y': 5} v = V({'oneOf': list(events.values())}) errs = list(v.iter_errors(bad)) print('oneOf: top-level errors:', len(errs), '|', [e.validator for e in errs], '|', errs[0].message[:60].replace('\n', ' ')) print(' context (the branches):', [(list(c.absolute_path), c.validator, c.message[:45]) for c in errs[0].context][:6]) print(' best_match ->', best_match(errs).message, '| path', list(best_match(errs).absolute_path)) chain = {'allOf': [{'if': {'properties': {'type': {'const': k}}, 'required': ['type']}, 'then': s} for k, s in events.items()]} e2 = list(V(chain).iter_errors(bad)); print('if/then chain: errors:', [(list(e.absolute_path), e.validator, e.message) for e in e2]) # jsonschema.validate() checks the schema first; V(...).is_valid does not import jsonschema try: jsonschema.validate({}, {'type': 'strng'}) except Exception as e: print('jsonschema.validate with a bad schema ->', type(e).__name__) print('V(bad schema).is_valid({}) ->', end=' ') try: print(V({'type': 'strng'}).is_valid({})) except Exception as e: print('raises', type(e).__name__, '-', str(e)[:70]) # human-readable error lines def lines(validator, doc): out = [] for e in sorted(validator.iter_errors(doc), key=lambda e: [str(p) for p in e.absolute_path]): where = '/'.join(str(p) for p in e.absolute_path) or '(whole document)' out.append(where + ': ' + e.message) return out orderv = V(json.load(open('order.schema.json', encoding='utf-8'))) print('\n'.join(lines(orderv, {'id': 0, 'customer': '', 'status': 'lost', 'date': '2026-10-01', 'items': [{'sku': 'a1', 'qty': 0, 'price': 0}]}))) Output: oneOf: top-level errors: 1 | ['oneOf'] | {'type': 'click', 'x': 'ten', 'y': 5} is not valid under any context (the branches): [(['x'], 'type', "'ten' is not of type 'integer'"), (['type'], 'const', "'key' was expected"), ([], 'required', "'key' is a required property"), (['type'], 'const', "'scroll' was expected"), ([], 'required', "'dy' is a required property")] best_match -> {'type': 'click', 'x': 'ten', 'y': 5} is not valid under any of the given schemas | path [] if/then chain: errors: [(['x'], 'type', "'ten' is not of type 'integer'")] jsonschema.validate with a bad schema -> SchemaError V(bad schema).is_valid({}) -> raises UnknownType - Unknown type 'strng' for validator with schema: {'type': 'strng'} customer: '' should be non-empty id: 0 is less than the minimum of 1 items/0/price: 0 is less than or equal to the minimum of 0 items/0/qty: 0 is less than the minimum of 1 items/0/sku: 'a1' does not match '^[A-Z][0-9]$' status: 'lost' is not one of ['pending', 'shipped', 'cancelled'] WHY THIS WORKS AS AN ANSWER --------------------------- Every method validates the same documents and reports the same number of valid ones (20,000/20,000, 200/200, 5,000/5,000), so the timings compare equal work. The ratios are the finding: compiled Ajv is 0.40 microseconds per document; creating a new Ajv and compiling per document is about 9.7 ms, roughly 24,000 times slower; re-registering a schema with the same $id throws. In Python, one validator object is cheap and is_valid is about 63 microseconds; jsonschema.validate() is about 2.4 ms because it checks the schema on every call. The t6 lines show the difference in what is checked when: validate() rejects a broken schema straight away (SchemaError), while a validator built directly raises only when the broken part is used (UnknownType) and ignores unknown keywords. Single runs, one machine, no warm-up control: do not quote the absolute numbers.