json1-6 Exercise 2: Schema Traps in Two Validators =================================================== 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 ex2.mjs and python ex2.py --- ex2.mjs --- // Exercise 2 (Node + Ajv 8.20.0): traps in schemas import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const Ajv2020 = require('ajv/dist/2020'); const addFormats = require('ajv-formats'); const line = (label, text) => console.log(label.padEnd(46) + text); const run = (schema, docs, opts = {}, formats = false) => { const ajv = new Ajv2020({ strict: false, ...opts }); if (formats) addFormats(ajv); const v = ajv.compile(schema); return docs.map(d => JSON.stringify(d) + '=' + (v(d) ? 'ok' : 'NO')).join(' '); }; console.log('== Ajv =='); line('type integer; JSON text 1.0, 1.5, 1e2', run({ type: 'integer' }, ['1.0', '1.5', '1e2'].map(s => JSON.parse(s)))); line('format "date", no plugin, validateFormats off', run({ type: 'string', format: 'date' }, ['2026-13-45'], { validateFormats: false })); line('format "date", ajv-formats added', run({ type: 'string', format: 'date' }, ['2026-13-45', '2026-10-12'], {}, true)); line('"required" with no "type": object', run({ required: ['a'] }, [{}, [], 'x'])); line('"required" with type: object', run({ type: 'object', required: ['a'] }, [{}, [], 'x'])); line('pattern "[0-9]" (not anchored)', run({ pattern: '[0-9]' }, ['a1', 'abc'])); line('maxLength 1 on one emoji', run({ maxLength: 1 }, ['\u{1F600}'])); line('multipleOf 0.01 (prices)', run({ multipleOf: 0.01 }, [19.99, 4.25, 0.07, 1.005, 0.1])); line('integer cents, multipleOf 1', run({ type: 'integer', minimum: 1 }, [1999, 425, 7, 100.5])); line('price as text: pattern ^[0-9]+\\.[0-9]{2}$', run({ type: 'string', pattern: '^[0-9]+\\.[0-9]{2}$' }, ['19.99', '4.250', '5'])); for (const [label, schema, opts] of [ ['default strict: unknown keyword "foo"', { type: 'object', foo: 1 }, {}], ['strict:false: unknown keyword "foo"', { type: 'object', foo: 1 }, { strict: false }], ['default strict: "properties" with no type', { properties: { a: {} } }, {}], ['format "date" with no plugin (default)', { type: 'string', format: 'date' }, {}], ]) { const logs = []; const ajv = new Ajv2020({ ...opts, logger: { log: m => logs.push('log: ' + m), warn: m => logs.push('warn: ' + m), error: m => logs.push('error: ' + m) } }); let res; try { ajv.compile(schema); res = 'compiled'; } catch (e) { res = 'THROWS ' + e.message.split('\n')[0]; } line(label, res + (logs.length ? ' [' + logs.join(' | ') + ']' : '')); } --- ex2.py --- # Exercise 2 (Python + jsonschema 4.26.0): the same traps import json from jsonschema import Draft202012Validator as V, FormatChecker def line(label, text): print(label.ljust(46) + text) def run(schema, docs, fmt=False): v = V(schema, format_checker=FormatChecker() if fmt else None) return ' '.join(json.dumps(d) + '=' + ('ok' if v.is_valid(d) else 'NO') for d in docs) print('== Python jsonschema ==') line('type integer; JSON text 1.0, 1.5, 1e2', run({'type': 'integer'}, [json.loads(s) for s in ['1.0', '1.5', '1e2']])) line('type integer; true', run({'type': 'integer'}, [True])) line('format "date", no format checker', run({'type': 'string', 'format': 'date'}, ['2026-13-45'])) line('format "date", FormatChecker()', run({'type': 'string', 'format': 'date'}, ['2026-13-45', '2026-10-12'], True)) line('"required" with no "type": object', run({'required': ['a']}, [{}, [], 'x'])) line('pattern "[0-9]" (not anchored)', run({'pattern': '[0-9]'}, ['a1', 'abc'])) line('maxLength 1 on one emoji', run({'maxLength': 1}, [chr(0x1F600)])) line('multipleOf 0.01 (prices)', run({'multipleOf': 0.01}, [19.99, 4.25, 0.07, 1.005, 0.1])) line('integer cents', run({'type': 'integer', 'minimum': 1}, [1999, 425, 7, 100.5])) line('unknown keyword "foo"', 'is_valid -> ' + str(V({'type': 'object', 'foo': 1}).is_valid({})) + ' (silently ignored)') for bad in [{'type': 'strng'}, {'minimum': 'x'}, {'required': 'a'}]: try: V.check_schema(bad); res = 'accepted' except Exception as e: res = 'check_schema rejects: ' + e.message line('check_schema(' + json.dumps(bad) + ')', res) Output of node ex2.mjs: == Ajv == type integer; JSON text 1.0, 1.5, 1e2 1=ok 1.5=NO 100=ok format "date", no plugin, validateFormats off "2026-13-45"=ok format "date", ajv-formats added "2026-13-45"=NO "2026-10-12"=ok "required" with no "type": object {}=NO []=ok "x"=ok "required" with type: object {}=NO []=NO "x"=NO pattern "[0-9]" (not anchored) "a1"=ok "abc"=NO maxLength 1 on one emoji "😀"=ok multipleOf 0.01 (prices) 19.99=NO 4.25=ok 0.07=NO 1.005=NO 0.1=ok integer cents, multipleOf 1 1999=ok 425=ok 7=ok 100.5=NO price as text: pattern ^[0-9]+\.[0-9]{2}$ "19.99"=ok "4.250"=NO "5"=NO default strict: unknown keyword "foo" THROWS strict mode: unknown keyword: "foo" strict:false: unknown keyword "foo" compiled default strict: "properties" with no type compiled [warn: strict mode: missing type "object" for keyword "properties" at "#" (strictTypes)] format "date" with no plugin (default) THROWS unknown format "date" ignored in schema at path "#" Output of python ex2.py: == Python jsonschema == type integer; JSON text 1.0, 1.5, 1e2 1.0=ok 1.5=NO 100.0=ok type integer; true true=NO format "date", no format checker "2026-13-45"=ok format "date", FormatChecker() "2026-13-45"=NO "2026-10-12"=ok "required" with no "type": object {}=NO []=ok "x"=ok pattern "[0-9]" (not anchored) "a1"=ok "abc"=NO maxLength 1 on one emoji "\ud83d\ude00"=ok multipleOf 0.01 (prices) 19.99=NO 4.25=ok 0.07=NO 1.005=NO 0.1=ok integer cents 1999=ok 425=ok 7=ok 100.5=NO unknown keyword "foo" is_valid -> True (silently ignored) check_schema({"type": "strng"}) check_schema rejects: 'strng' is not valid under any of the given schemas check_schema({"minimum": "x"}) check_schema rejects: 'x' is not of type 'number' check_schema({"required": "a"}) check_schema rejects: 'a' is not of type 'array' WHY THIS WORKS AS AN ANSWER --------------------------- Each trap is shown in both libraries, side by side, so a difference between them is visible instead of assumed. The results: an integer type accepts the JSON text 1.0 and 1e2 (Python prints them as 1.0 and 100.0, Node as 1 and 100 because JavaScript has one number type); format is ignored unless a plugin or checker is added, and Ajv's default options throw on an unknown format instead of ignoring it; required without type: object accepts [] and "x"; pattern is unanchored; multipleOf 0.01 rejects 19.99 and 0.07 in both libraries (binary floating point), while integer cents and a string with an anchored pattern are exact. The string alternative has a control, "1x99", that must fail (it does: the dot is escaped; my first version had an unescaped dot and would have accepted it). Ajv throws on an unknown keyword by default; Python ignores it, and Draft202012Validator.check_schema is what catches {type: strng}. Tested only with these two libraries and their default options (Ajv options are shown in the script).