json1-6 Exercise 3: Do Two Validators Agree? ============================================= Run with Node 24.18.0 with Ajv 8.20.0 (ajv/dist/2020) and ajv-formats 3.0.1; Python 3.14.0 with jsonschema 4.26.0, both installed in a scratch folder (npm install ajv ajv-formats; py -m venv .venv; pip install jsonschema): node ex3.mjs ; python ex3.py ; python ex3_compare.py ; node t5.mjs (the $ref and class errors) ; node t7.mjs (allErrors off and on) --- order.schema.json --- { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/order.json", "title": "Order", "type": "object", "properties": { "id": { "type": "integer", "minimum": 1 }, "customer": { "type": "string", "minLength": 1 }, "status": { "enum": ["pending", "shipped", "cancelled"] }, "date": { "type": "string", "format": "date" }, "items": { "type": "array", "items": { "$ref": "#/$defs/item" } } }, "required": ["id", "customer", "status", "date", "items"], "additionalProperties": false, "$defs": { "item": { "type": "object", "properties": { "sku": { "type": "string", "pattern": "^[A-Z][0-9]$" }, "qty": { "type": "integer", "minimum": 1 }, "price": { "type": "number", "exclusiveMinimum": 0 } }, "required": ["sku", "qty", "price"], "additionalProperties": false } } } --- cases.json --- { "valid order": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01","items":[{"sku":"A1","qty":2,"price":3.5}]}, "no items": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01"}, "id is a string": {"id":"1","customer":"ada","status":"pending","date":"2026-10-01","items":[]}, "id is 0": {"id":0,"customer":"ada","status":"pending","date":"2026-10-01","items":[]}, "bad status": {"id":1,"customer":"ada","status":"lost","date":"2026-10-01","items":[]}, "extra field": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01","items":[],"note":"x"}, "bad date": {"id":1,"customer":"ada","status":"pending","date":"2026-13-45","items":[]}, "bad sku": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01","items":[{"sku":"a1","qty":1,"price":2}]}, "qty 0 and price 0": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01","items":[{"sku":"A1","qty":0,"price":0}]}, "item missing price": {"id":1,"customer":"ada","status":"pending","date":"2026-10-01","items":[{"sku":"A1","qty":1}]}, "empty customer": {"id":1,"customer":"","status":"pending","date":"2026-10-01","items":[]}, "an array, not an object": [], "null": null, "everything wrong": {"id":0,"customer":"","status":"lost","date":"2026-10-01","items":[{"sku":"a1","qty":0,"price":0}],"note":1} } --- ex3.mjs --- import { make, load } from './v.mjs'; import fs from 'node:fs'; const v = make({ allErrors: true }).compile(load('order.schema.json')); const out = {}; for (const [label, doc] of Object.entries(load('cases.json'))) { v(doc); out[label] = (v.errors || []).map(e => (e.instancePath || '/') + ' ' + e.keyword).sort(); } fs.writeFileSync('ajv_out.json', JSON.stringify(out, null, 1)); console.log('Ajv wrote results for ' + Object.keys(out).length + ' cases'); --- ex3.py --- import json from jsonschema import Draft202012Validator as V, FormatChecker v = V(json.load(open('order.schema.json', encoding='utf-8')), format_checker=FormatChecker()) out = {} for label, doc in json.load(open('cases.json', encoding='utf-8')).items(): out[label] = sorted(('/' + '/'.join(str(p) for p in e.absolute_path)) + ' ' + e.validator for e in v.iter_errors(doc)) json.dump(out, open('py_out.json', 'w', encoding='utf-8'), indent=1) print('Python wrote results for', len(out), 'cases') --- ex3_compare.py --- import json a = json.load(open('ajv_out.json', encoding='utf-8')); p = json.load(open('py_out.json', encoding='utf-8')) def compare(a, p): return [k for k in a if a[k] != p.get(k)] print('case'.ljust(26) + 'verdict'.ljust(10) + 'errors (keyword at path)') for k in a: same = a[k] == p[k] print(k.ljust(26) + ('INVALID' if a[k] else 'valid').ljust(10) + ('; '.join(a[k]) if a[k] else '-') + ('' if same else ' <-- Python says: ' + '; '.join(p[k]))) print() print('cases where the two validators disagree:', compare(a, p) or 'none') # control: damage one result on purpose and make sure the comparison notices p2 = json.loads(json.dumps(p)); p2['bad sku'] = [] print('control (one Python result deleted on purpose):', compare(a, p2)) Output (all three scripts, in order): Ajv wrote results for 14 cases Python wrote results for 14 cases case verdict errors (keyword at path) valid order valid - no items INVALID / required id is a string INVALID /id type id is 0 INVALID /id minimum bad status INVALID /status enum extra field INVALID / additionalProperties bad date INVALID /date format bad sku INVALID /items/0/sku pattern qty 0 and price 0 INVALID /items/0/price exclusiveMinimum; /items/0/qty minimum item missing price INVALID /items/0 required empty customer INVALID /customer minLength an array, not an object INVALID / type null INVALID / type everything wrong INVALID / additionalProperties; /customer minLength; /id minimum; /items/0/price exclusiveMinimum; /items/0/qty minimum; /items/0/sku pattern; /status enum cases where the two validators disagree: none control (one Python result deleted on purpose): ['bad sku'] --- t7.mjs (allErrors off and on) --- 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 doc = { id: 0, customer: '', status: 'lost', date: '2026-10-01', items: [{ sku: 'a1', qty: 0, price: 0 }], note: 1 }; for (const allErrors of [false, true]) { const ajv = new Ajv2020({ allErrors }); addFormats(ajv); const v = ajv.compile(schema); v(doc); console.log('allErrors: ' + allErrors + ' -> ' + v.errors.length + ' error(s)'); for (const e of v.errors) console.log(' ' + e.instancePath.padEnd(14) + e.keyword.padEnd(22) + e.message); } Output: allErrors: false -> 1 error(s) additionalProperties must NOT have additional properties allErrors: true -> 7 error(s) additionalProperties must NOT have additional properties /id minimum must be >= 1 /customer minLength must NOT have fewer than 1 characters /status enum must be equal to one of the allowed values /items/0/sku pattern must match pattern "^[A-Z][0-9]$" /items/0/qty minimum must be >= 1 /items/0/priceexclusiveMinimum must be > 0 --- t5.mjs (class, $ref and $id errors) --- import { createRequire } from 'node:module'; import fs from 'node:fs'; const require = createRequire(import.meta.url); const Ajv2020 = require('ajv/dist/2020'); const Ajv = require('ajv'); const addFormats = require('ajv-formats'); const schema = JSON.parse(fs.readFileSync('order.schema.json', 'utf8')); console.log('--- default Ajv class (draft-07) given a 2020-12 $schema'); try { new Ajv().compile(schema); console.log('compiled'); } catch (e) { console.log('ERROR ' + e.message.split('\n')[0]); } const doc = { id: 0, customer: '', status: 'lost', date: '2026-10-01', items: [{ sku: 'a1', qty: 0, price: 0 }], note: 1 }; for (const allErrors of [false, true]) { const ajv = new Ajv2020({ allErrors }); addFormats(ajv); const v = ajv.compile(schema); v(doc); console.log('--- allErrors: ' + allErrors + ' -> ' + v.errors.length + ' error(s)'); for (const e of v.errors) console.log(' ' + JSON.stringify(e)); if (!allErrors) break; } console.log('--- $ref resolution'); const ajv = new Ajv2020(); ajv.addSchema({ $id: 'https://example.com/schemas/sku.json', type: 'string', pattern: '^[A-Z][0-9]$' }); const v2 = ajv.compile({ type: 'object', properties: { sku: { $ref: 'https://example.com/schemas/sku.json' } } }); console.log('ref to a schema added by $id:', v2({ sku: 'A1' }), v2({ sku: 'zz' })); try { new Ajv2020().compile({ properties: { a: { $ref: '#/$defs/missing' } } }); } catch (e) { console.log('missing $ref ->', e.message); } try { new Ajv2020().compile({ properties: { a: { $ref: 'https://example.com/nope.json' } } }); } catch (e) { console.log('unknown $id ->', e.message); } const v3 = new Ajv2020({ strict: false }).compile({ $defs: { node: { type: 'object', properties: { name: { type: 'string' }, children: { type: 'array', items: { $ref: '#/$defs/node' } } }, required: ['name'] } }, $ref: '#/$defs/node' }); console.log('recursive schema (a tree):', v3({ name: 'a', children: [{ name: 'b', children: [{ name: 'c' }] }] }), v3({ name: 'a', children: [{ name: 'b', children: [{}] }] })); console.log('--- $anchor'); const v4 = new Ajv2020({ strict: false }).compile({ $defs: { n: { $anchor: 'num', type: 'integer' } }, properties: { a: { $ref: '#num' } } }); console.log('$anchor ref:', v4({ a: 1 }), v4({ a: 'x' })); Output: --- default Ajv class (draft-07) given a 2020-12 $schema ERROR no schema with key or ref "https://json-schema.org/draft/2020-12/schema" --- allErrors: false -> 1 error(s) {"instancePath":"","schemaPath":"#/additionalProperties","keyword":"additionalProperties","params":{"additionalProperty":"note"},"message":"must NOT have additional properties"} --- $ref resolution ref to a schema added by $id: true false strict mode: missing type "object" for keyword "properties" at "#" (strictTypes) missing $ref -> can't resolve reference #/$defs/missing from id # strict mode: missing type "object" for keyword "properties" at "#" (strictTypes) unknown $id -> can't resolve reference https://example.com/nope.json from id # recursive schema (a tree): true false --- $anchor $anchor ref: true false WHY THIS WORKS AS AN ANSWER --------------------------- The comparison script uses the keyword and the path of every error, not the message text, because messages differ between libraries. The two validators agreed on all 14 cases. The control (one Python result deleted on purpose) is reported as a disagreement on exactly that case, so "none" in the real comparison means something. The t7 output shows Ajv stopping at the first error without allErrors (1 error) and finding all seven with it. The t5 output shows three Ajv compile-time failures: the default (draft-07) class given a 2020-12 $schema, a $ref to a missing #/$defs entry and a $ref to an unknown $id, plus the two things that do work (a schema added with addSchema under its $id; a recursive tree through $ref). Not tested: other validators, and the older drafts.