ata-validator 1.31.0 → 1.31.2

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.
package/compat.js CHANGED
@@ -22,7 +22,7 @@ const { resolvePointer } = require('./lib/pointer.js');
22
22
 
23
23
  const { Validator } = require('./index');
24
24
  const { METASCHEMAS } = require('./lib/metaschemas');
25
- const { createShaper } = require('./lib/compat-errors');
25
+ const { createShaper, asReferenceKeyword } = require('./lib/compat-errors');
26
26
 
27
27
  const DRAFT7 = 'http://json-schema.org/draft-07/schema#';
28
28
  const DRAFT2020 = 'https://json-schema.org/draft/2020-12/schema';
@@ -185,7 +185,7 @@ class Ata {
185
185
  validate.errors = null;
186
186
  return true;
187
187
  }
188
- let errors = shaper === null ? result.errors : shaper(result.errors, data);
188
+ let errors = shaper === null ? result.errors.map(asReferenceKeyword) : shaper(result.errors, data);
189
189
  // Errors the shaper synthesises, the `if` wrapper among them, are built
190
190
  // from the group rather than carried up from the validator, so verbose
191
191
  // mode has to fill in the value they point at here or they would be the
package/index.js CHANGED
@@ -1406,7 +1406,7 @@ class Validator {
1406
1406
  return this._compiled.validate(x);
1407
1407
  });
1408
1408
  }
1409
- return _errOnlyImpl(d);
1409
+ return _mustReject(_errOnlyImpl(d));
1410
1410
  };
1411
1411
 
1412
1412
  // Best path: combined validator (single pass, validates + collects errors)
@@ -1450,7 +1450,7 @@ class Validator {
1450
1450
  let _errPreferredImpl = null;
1451
1451
  const errPreferCombined = (d) => {
1452
1452
  if (_errPreferredImpl === null) _errPreferredImpl = combinedIfSafe() || errOnly;
1453
- return _errPreferredImpl(d);
1453
+ return _mustReject(_errPreferredImpl(d));
1454
1454
  };
1455
1455
 
1456
1456
  // The boolean engine is the verdict authority for these paths; the
@@ -1513,7 +1513,7 @@ class Validator {
1513
1513
  let impl = null;
1514
1514
  const onReject = (data) => {
1515
1515
  const combined = combinedIfSafe();
1516
- if (combined) { impl = combined; return combined(data); }
1516
+ if (combined) { impl = combined; return _mustReject(combined(data)); }
1517
1517
  return errOnly(data);
1518
1518
  };
1519
1519
  const hybridFn = jsFn._hybridFactory(VALID_RESULT, onReject);
@@ -2666,6 +2666,19 @@ Object.defineProperty(Validator.prototype, "~standard", {
2666
2666
  },
2667
2667
  });
2668
2668
 
2669
+ // The error resolvers run only after a verdict function has said no. If one
2670
+ // answers valid anyway, two generators disagree, and the verdict is the one
2671
+ // to keep: returning the resolver's answer is how a vacuous combined function
2672
+ // turned a rejection into an acceptance in validateJSON. The disagreement is
2673
+ // reported as a generic failure rather than hidden.
2674
+ const _VERDICT_DISAGREES = Object.freeze({
2675
+ valid: false,
2676
+ errors: Object.freeze([Object.freeze({ keyword: 'validation', instancePath: '', schemaPath: '#', params: Object.freeze({}), message: 'schema validation failed' })]),
2677
+ });
2678
+ function _mustReject(r) {
2679
+ return r && r.valid === false ? r : _VERDICT_DISAGREES;
2680
+ }
2681
+
2669
2682
  // Install the verdict method. Every place that binds isValidObject comes
2670
2683
  // through here, so a check registered with _extendVerdict survives the method
2671
2684
  // being replaced as the validator compiles further, which it does more than
@@ -133,6 +133,16 @@ function escapePointer(s) {
133
133
  // validate() already reports them so.
134
134
  const WRAPPERS = new Set(['anyOf', 'oneOf', 'propertyNames', 'contains', 'not', 'if']);
135
135
 
136
+ // ata names the keyword of a false schema `not`, at the root and nested alike,
137
+ // which is what its own error codes and every consumer switch on. The
138
+ // reference names it `false schema`. The root case used to come out of the
139
+ // code generator as `false schema` by an inconsistency that has since been
140
+ // fixed, so this shim matched the reference there and not for a nested false
141
+ // schema; the rename here makes both match.
142
+ function asReferenceKeyword(e) {
143
+ return e.keyword === 'not' && e.message === 'boolean schema is false' ? { ...e, keyword: 'false schema' } : e;
144
+ }
145
+
136
146
  function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors, macroKeywords }) {
137
147
  const branchCache = new Map();
138
148
  const branchValidator = (pointer) => {
@@ -360,11 +370,11 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
360
370
  }
361
371
 
362
372
  return function shapeErrors(errors, data) {
363
- const shaped = shape(errors, data, '');
373
+ const shaped = shape(errors, data, '').map(asReferenceKeyword);
364
374
  if (allErrors) return shaped;
365
375
  const n = groupEnd(shaped);
366
376
  return shaped.slice(0, n);
367
377
  };
368
378
  }
369
379
 
370
- module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER };
380
+ module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER, asReferenceKeyword };
@@ -157,10 +157,31 @@ const DOC_BASE = 'https://ata-validator.com/e/'
157
157
  // reader sorts by without deriving it from the path. Empty when the path is
158
158
  // not into the root document.
159
159
  function ordinalField (ctx, schemaPath) {
160
- const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, schemaPath) : null
160
+ const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
161
161
  return o === null ? '' : `,_o:${o}`
162
162
  }
163
163
 
164
+ // A schema path as the generators hold it is text for a single-quoted source
165
+ // literal: ptrSeg put a backslash before every backslash and quote in a key.
166
+ // Anything that uses the path as a value, the ordinal lookup and the frozen
167
+ // error objects below, has to take that backslash off again, or a key such as
168
+ // `it's` reads as `it\'s` and the error names a path that does not exist.
169
+ // The value of a string literal the generators built for source. One that does
170
+ // not parse on its own (a key with a raw line break) would not parse inside the
171
+ // generated function either, so the generator declines, as it would have when
172
+ // that function failed to build.
173
+ function literalValue (lit) {
174
+ try {
175
+ return Function('return ' + lit)()
176
+ } catch {
177
+ throw DECLINE
178
+ }
179
+ }
180
+
181
+ function unescapeSp (sp) {
182
+ return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
183
+ }
184
+
164
185
  function buildErrorLiteral (opts) {
165
186
  const { keyword, format, schemaPath, sourceMap } = opts
166
187
  let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
@@ -340,7 +361,7 @@ function compileToJS(schema, defs, schemaMap) {
340
361
  // required applies to objects only; other types are ignored.
341
362
  if (schema.required && Array.isArray(schema.required)) {
342
363
  for (const key of schema.required) {
343
- checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || d[key] !== undefined)
364
+ checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || hasOwnKey(d, key))
344
365
  }
345
366
  }
346
367
 
@@ -350,7 +371,7 @@ function compileToJS(schema, defs, schemaMap) {
350
371
  const propCheck = compileToJS(prop, rootDefs)
351
372
  if (!propCheck) return null // bail if sub-schema too complex
352
373
  checks.push((d) => {
353
- if (typeof d !== 'object' || d === null || !(key in d)) return true
374
+ if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
354
375
  return propCheck(d[key])
355
376
  })
356
377
  }
@@ -387,9 +408,9 @@ function compileToJS(schema, defs, schemaMap) {
387
408
  if (schema.dependentRequired) {
388
409
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
389
410
  checks.push((d) => {
390
- if (typeof d !== 'object' || d === null || !(key in d)) return true
411
+ if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
391
412
  for (let i = 0; i < deps.length; i++) {
392
- if (!(deps[i] in d)) return false
413
+ if (!hasOwnKey(d, deps[i])) return false
393
414
  }
394
415
  return true
395
416
  })
@@ -872,6 +893,44 @@ const FORMAT_CHECKS = {
872
893
  }
873
894
 
874
895
  // Dangerous JS property names that exist on Object.prototype
896
+ // Presence is an own-property question: `required: ['toString']` is not
897
+ // satisfied by `{}`. `'k' in v` and `v.k !== undefined` both answer yes for a
898
+ // name found on the prototype chain, which accepted documents the interpreted
899
+ // engine rejects. Object.hasOwn would be exact but is not inlined, about 5 ns
900
+ // a key against well under 1 for `in`, so the check is split:
901
+ // - a name on Object.prototype (and `__proto__`) is always checked with
902
+ // hasOwnProperty; these are rare in schemas and known when compiling;
903
+ // - any other name can only be inherited from a prototype that is not
904
+ // Object.prototype, so an ordinary object answers with `in` and one
905
+ // comparison of its prototype, and anything else, a class instance, a
906
+ // null-prototype object, one from another realm, pays for hasOwnProperty.
907
+ // `v.__proto__` is read rather than Object.getPrototypeOf(v) because the engine
908
+ // inlines the accessor and not the call (0.8 ns against 8 for eleven keys). An
909
+ // object whose own `__proto__` is a data property reads that value, which is
910
+ // not Object.prototype, so it takes the exact path.
911
+ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__'])
912
+ // Declared once per compiled function and called by name: spelled out at each
913
+ // property, the test grew a 50-property module by a tenth. Both are small
914
+ // enough for the engine to inline.
915
+ const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;' +
916
+ 'function _own(o,k){return o.__proto__===Object.prototype?k in o:_hop.call(o,k)}'
917
+ function ownKeyExpr (ctx, v, key) {
918
+ hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
919
+ const k = JSON.stringify(key)
920
+ return PROTO_NAMES.has(key) ? `_hop.call(${v},${k})` : `_own(${v},${k})`
921
+ }
922
+ // For a value already read as `v[key]` and found not to be undefined: whether
923
+ // it came from `v` itself. Same test; `in` is already known to hold.
924
+ function ownGuard (ctx, v, key) {
925
+ return ownKeyExpr(ctx, v, key)
926
+ }
927
+ // The same test at run time, for the closure compiler.
928
+ const _hop = Object.prototype.hasOwnProperty
929
+ function hasOwnKey (d, key) {
930
+ if (PROTO_NAMES.has(key)) return _hop.call(d, key)
931
+ return key in d && (d.__proto__ === Object.prototype || _hop.call(d, key))
932
+ }
933
+
875
934
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
876
935
  'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
877
936
 
@@ -968,6 +1027,23 @@ function hasNestedIdScope(node, isRoot = true) {
968
1027
  // only safe way out.
969
1028
  const DECLINE = Symbol('ata.decline')
970
1029
 
1030
+ // The error and combined generators compile the subschemas of not, if, contains
1031
+ // and their anyOf/oneOf closures with the boolean generator. That generator
1032
+ // follows a reference back to the root with `_validate` and a reference cycle
1033
+ // with a named function in its preamble, and neither exists inside the other
1034
+ // two generators' functions: the first threw a ReferenceError on the first
1035
+ // document that reached it, and the second cut the cycle, so the reference
1036
+ // checked nothing. While `nestedBoolean` is set, genCode declines at those
1037
+ // points instead, and the caller's generator declines the schema.
1038
+ function nestedGenCode(schema, v, lines, ctx) {
1039
+ ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
1040
+ try {
1041
+ genCode(schema, v, lines, ctx)
1042
+ } finally {
1043
+ ctx.nestedBoolean--
1044
+ }
1045
+ }
1046
+
971
1047
  const CYCLE_DEPTH = 512
972
1048
 
973
1049
  // Declarations the guarded functions close over. Emitted once, ahead of them.
@@ -1878,6 +1954,7 @@ function genCode(schema, v, lines, ctx, knownType) {
1878
1954
  if (schema.$ref) {
1879
1955
  // Self-reference "#" — recursive call to root validator
1880
1956
  if (schema.$ref === '#') {
1957
+ if (ctx.nestedBoolean) throw DECLINE
1881
1958
  ctx.usesRecursion = true
1882
1959
  lines.push(`if(!_validate(${v}))return false`)
1883
1960
  if (!hasSiblings) return
@@ -1920,7 +1997,7 @@ function genCode(schema, v, lines, ctx, knownType) {
1920
1997
  }
1921
1998
  lines.push(`if(!${fnName}(${v}))return false`)
1922
1999
  if (!hasSiblings) return
1923
- } else if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
2000
+ } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return }
1924
2001
  else {
1925
2002
  ctx.refStack.add(schema.$ref)
1926
2003
  genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
@@ -1932,7 +2009,7 @@ function genCode(schema, v, lines, ctx, knownType) {
1932
2009
  const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
1933
2010
  const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
1934
2011
  if (anchorTarget) {
1935
- if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
2012
+ if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return }
1936
2013
  else {
1937
2014
  ctx.refStack.add(schema.$ref)
1938
2015
  genCode(anchorTarget, v, lines, ctx, knownType)
@@ -1975,11 +2052,13 @@ function genCode(schema, v, lines, ctx, knownType) {
1975
2052
  const target = ctx.anchors[anchorKey]
1976
2053
  if (target === ctx.rootSchema) {
1977
2054
  // Self-recursive: generate _validate(v) call
2055
+ if (ctx.nestedBoolean) throw DECLINE
1978
2056
  ctx.usesRecursion = true
1979
2057
  lines.push(`if(!_validate(${v}))return false`)
1980
2058
  } else {
1981
2059
  // Different schema: inline the target validation
1982
2060
  const refKey = '$dynamicRef:' + anchorKey
2061
+ if (ctx.refStack.has(refKey) && ctx.nestedBoolean) throw DECLINE
1983
2062
  if (!ctx.refStack.has(refKey)) {
1984
2063
  ctx.refStack.add(refKey)
1985
2064
  genCode(target, v, lines, ctx, knownType)
@@ -2057,24 +2136,34 @@ function genCode(schema, v, lines, ctx, knownType) {
2057
2136
  const hoisted = {} // key -> access expression
2058
2137
  if (schema.required && schema.properties && isObj) {
2059
2138
  const reqChecks = []
2139
+ const typed = []
2060
2140
  for (const key of schema.required) {
2061
2141
  hoisted[key] = `${v}[${JSON.stringify(key)}]`
2062
2142
  const prop = schema.properties[key]
2063
2143
  const hasTypeCheck = prop && (prop.type || prop.enum || prop.const !== undefined)
2064
- if (!hasTypeCheck) {
2065
- reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined`)
2144
+ // A typed property needs no separate presence check on an ordinary
2145
+ // object: its type check fails on undefined. A name on Object.prototype
2146
+ // is never absent that way, and it is checked explicitly.
2147
+ if (!hasTypeCheck || PROTO_NAMES.has(key)) {
2148
+ reqChecks.push(`!${ownKeyExpr(ctx, v, key)}`)
2149
+ } else {
2150
+ typed.push(key)
2066
2151
  }
2067
2152
  }
2068
2153
  if (reqChecks.length > 0) {
2069
2154
  lines.push(`if(${reqChecks.join('||')})return false`)
2070
2155
  }
2156
+ // On an object with another prototype the typed ones may be inherited.
2157
+ if (typed.length > 0) {
2158
+ hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
2159
+ lines.push(`if(${v}.__proto__!==Object.prototype&&!(${typed.map((k) => `_hop.call(${v},${JSON.stringify(k)})`).join('&&')}))return false`)
2160
+ }
2071
2161
  } else if (schema.required && schema.required.length > 0) {
2162
+ const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
2072
2163
  if (isObj) {
2073
- const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
2074
2164
  lines.push(`if(${checks.join('||')})return false`)
2075
2165
  } else {
2076
2166
  // required applies to objects only; other types are ignored.
2077
- const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
2078
2167
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${checks.join('||')}))return false`)
2079
2168
  }
2080
2169
  }
@@ -2267,8 +2356,8 @@ function genCode(schema, v, lines, ctx, knownType) {
2267
2356
  // dependentRequired
2268
2357
  if (schema.dependentRequired) {
2269
2358
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
2270
- const depChecks = deps.map(d => `!('${esc(d)}' in ${v})`).join('||')
2271
- lines.push(`if(${objGuard}'${esc(key)}' in ${v}&&(${depChecks}))return false`)
2359
+ const depChecks = deps.map(d => `!${ownKeyExpr(ctx, v, d)}`).join('||')
2360
+ lines.push(`if(${objGuard}${ownKeyExpr(ctx, v, key)}&&(${depChecks}))return false`)
2272
2361
  }
2273
2362
  }
2274
2363
 
@@ -2405,7 +2494,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2405
2494
  if (schema.dependentSchemas) {
2406
2495
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
2407
2496
  const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
2408
- lines.push(`if(${guard}${JSON.stringify(key)} in ${v}){`)
2497
+ lines.push(`if(${guard}${ownKeyExpr(ctx, v, key)}){`)
2409
2498
  ctx.condDepth = (ctx.condDepth || 0) + 1
2410
2499
  genCode(depSchema, v, lines, ctx, effectiveType)
2411
2500
  ctx.condDepth--
@@ -2471,7 +2560,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2471
2560
  // Optional — hoist to local, check undefined
2472
2561
  const oi = ctx.varCounter++
2473
2562
  const local = `_o${oi}`
2474
- lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
2563
+ lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
2475
2564
  const combined = tryGenCombined(prop, local, ctx)
2476
2565
  if (combined) {
2477
2566
  lines.push(combined)
@@ -2480,7 +2569,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2480
2569
  }
2481
2570
  lines.push(`}}`)
2482
2571
  } else {
2483
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}){`)
2572
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
2484
2573
  genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
2485
2574
  lines.push(`}`)
2486
2575
  }
@@ -2844,7 +2933,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2844
2933
  for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
2845
2934
  const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
2846
2935
  if (depResult.props && depResult.props.length > 0) {
2847
- lines.push(`if(${JSON.stringify(trigger)} in ${v}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
2936
+ lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
2848
2937
  }
2849
2938
  }
2850
2939
  const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
@@ -3442,7 +3531,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3442
3531
  if (typeof schema === 'boolean') {
3443
3532
  return schema
3444
3533
  ? () => ({ valid: true, errors: [] })
3445
- : () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
3534
+ : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
3446
3535
  }
3447
3536
  if (typeof schema !== 'object' || schema === null) return null
3448
3537
  if (!sharedCodegenGate(schema, schemaMap)) return null
@@ -3803,7 +3892,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3803
3892
  const reqSp = `${schemaPrefix}/required`
3804
3893
  const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
3805
3894
  for (const key of schema.required) {
3806
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
3895
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
3807
3896
  }
3808
3897
  }
3809
3898
 
@@ -4020,7 +4109,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4020
4109
  const drLit = buildErrorLiteral({ keyword: 'dependentRequired', schemaPath: drSp, sourceMap: ctx.sourceMap })
4021
4110
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
4022
4111
  for (const dep of deps) {
4023
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){_e.push({code:'${drLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${drSp}'${ordinalField(ctx, `${drSp}`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'",docUrl:'${drLit.docUrl}'${drLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
4112
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){_e.push({code:'${drLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${drSp}'${ordinalField(ctx, `${drSp}`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'",docUrl:'${drLit.docUrl}'${drLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
4024
4113
  }
4025
4114
  }
4026
4115
  }
@@ -4029,7 +4118,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4029
4118
  if (schema.properties) {
4030
4119
  for (const [key, prop] of Object.entries(schema.properties)) {
4031
4120
  const childPath = childPathExpr(pathExpr, ptrSeg(key))
4032
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4121
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4033
4122
  genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4034
4123
  lines.push(`}`)
4035
4124
  }
@@ -4098,7 +4187,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4098
4187
  // dependentSchemas
4099
4188
  if (schema.dependentSchemas) {
4100
4189
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
4101
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4190
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4102
4191
  genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
4103
4192
  lines.push(`}`)
4104
4193
  }
@@ -4178,7 +4267,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4178
4267
  if (schema.contains !== undefined) {
4179
4268
  const ci = ctx.varCounter++
4180
4269
  const subLines = []
4181
- genCode(schema.contains, `_cv`, subLines, ctx)
4270
+ nestedGenCode(schema.contains, `_cv`, subLines, ctx)
4182
4271
  const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
4183
4272
  const minC = schema.minContains !== undefined ? schema.minContains : 1
4184
4273
  const maxC = schema.maxContains
@@ -4210,7 +4299,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4210
4299
  // not
4211
4300
  if (schema.not !== undefined) {
4212
4301
  const subLines = []
4213
- genCode(schema.not, '_nv', subLines, ctx)
4302
+ nestedGenCode(schema.not, '_nv', subLines, ctx)
4214
4303
  const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
4215
4304
  const fi = ctx.varCounter++
4216
4305
  lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
@@ -4219,7 +4308,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4219
4308
  // if/then/else
4220
4309
  if (schema.if !== undefined) {
4221
4310
  const ifLines = []
4222
- genCode(schema.if, '_iv', ifLines, ctx)
4311
+ nestedGenCode(schema.if, '_iv', ifLines, ctx)
4223
4312
  const fi = ctx.varCounter++
4224
4313
  const ifFn = ifLines.length === 0
4225
4314
  ? `function(_iv){return true}`
@@ -4266,7 +4355,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
4266
4355
  if (typeof schema === 'boolean') {
4267
4356
  return schema
4268
4357
  ? () => VALID_RESULT
4269
- : () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
4358
+ : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
4270
4359
  }
4271
4360
  if (typeof schema !== 'object' || schema === null) return null
4272
4361
  if (!sharedCodegenGate(schema, schemaMap)) return null
@@ -4324,22 +4413,34 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
4324
4413
  const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
4325
4414
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema, userFormats: userFormats || null }
4326
4415
  const lines = []
4327
- genCodeC(schema, 'd', '', lines, ctx, '#')
4416
+ try {
4417
+ genCodeC(schema, 'd', '', lines, ctx, '#')
4418
+ } catch (e) {
4419
+ if (e === DECLINE) return null
4420
+ throw e
4421
+ }
4328
4422
  if (lines.length === 0) return () => VALID_RESULT
4329
4423
 
4330
- // Use factory pattern: closure vars (regexes, etc.) created once, not per call
4331
- const closureParams = ctx.closureVars.join(',')
4332
- // Lazy error array — no allocation for valid data (the common case)
4424
+ // Use factory pattern: closure vars (regexes, etc.) created once, not per call.
4425
+ // The helpers are constants too and are declared in the factory: inside the
4426
+ // function they were rebuilt on every call. `__ataSafeRe` is bound when a
4427
+ // helper compiles a pattern through it, which the boolean generator does for
4428
+ // subschemas under not/if/contains; without the binding those threw a
4429
+ // ReferenceError on the first document that reached them.
4430
+ const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
4431
+ const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
4432
+ const closureParams = closureNames.join(',')
4433
+ const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
4434
+ // Lazy error array: no allocation for valid data (the common case)
4333
4435
  const inner = `let _e;\n ` +
4334
- (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '') +
4335
4436
  lines.join('\n ') +
4336
4437
  `\n return _e?{valid:false,errors:_e}:R`
4337
4438
 
4338
4439
  try {
4339
- if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
4440
+ if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
4340
4441
  const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
4341
- `return function(d){${inner}}`)
4342
- return factory(VALID_RESULT, ...ctx.closureVals)
4442
+ `${helpers}return function(d){${inner}}`)
4443
+ return factory(VALID_RESULT, ...closureArgs)
4343
4444
  } catch (e) {
4344
4445
  if (typeof process !== 'undefined' && process.env && process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
4345
4446
  return null
@@ -4411,17 +4512,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4411
4512
  const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
4412
4513
  if (ctx.anchors && ctx.anchors[anchorKey]) {
4413
4514
  const target = ctx.anchors[anchorKey]
4414
- if (target === ctx.rootSchema) {
4415
- // Self-recursive: bail to non-combined path (combined doesn't support named recursion)
4416
- // Just skip — the hybrid path will handle this via jsFn + errFn
4417
- } else {
4418
- const refKey = '$dynamicRef:' + anchorKey
4419
- if (!ctx.refStack.has(refKey)) {
4420
- ctx.refStack.add(refKey)
4421
- genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
4422
- ctx.refStack.delete(refKey)
4423
- }
4424
- }
4515
+ // This generator has no named functions, so it cannot follow a reference
4516
+ // back to the root or around a cycle. It used to emit nothing in both
4517
+ // cases, which the function then answered as valid: a document the other
4518
+ // generators reject passed through validateJSON. It declines instead,
4519
+ // and the schema takes the error generator.
4520
+ if (target === ctx.rootSchema) throw DECLINE
4521
+ const refKey = '$dynamicRef:' + anchorKey
4522
+ if (ctx.refStack.has(refKey)) throw DECLINE
4523
+ ctx.refStack.add(refKey)
4524
+ genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
4525
+ ctx.refStack.delete(refKey)
4425
4526
  }
4426
4527
  }
4427
4528
 
@@ -4441,14 +4542,28 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4441
4542
  // Static error: pre-allocate as frozen closure variable
4442
4543
  const ei = ctx.varCounter++
4443
4544
  const errVar = `_E${ei}`
4444
- const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
4445
- const msgVal = msgCode.slice(1, -1)
4545
+ // The path and message are source literals; evaluate them rather than
4546
+ // strip the quotes, so an escaped quote or backslash in a key comes
4547
+ // out as the character and not as the escape.
4548
+ let pathVal, msgVal
4549
+ try {
4550
+ pathVal = pathExpr ? Function('return ' + pathExpr)() : ''
4551
+ msgVal = Function('return ' + msgCode)()
4552
+ } catch {
4553
+ // Not a literal that parses on its own (a key with a raw line break):
4554
+ // leave it to the inline form below, which declines the same way the
4555
+ // whole function would.
4556
+ pathVal = undefined
4557
+ }
4558
+ if (pathVal !== undefined) {
4559
+ const spVal = unescapeSp(sp)
4446
4560
  ctx.closureVars.push(errVar)
4447
- const o = ordinalFor(ctx.rootSchema, sp)
4448
- const lit = {keyword, instancePath: pathVal, schemaPath: sp, params: Object.freeze(paramsVal), message: msgVal}
4561
+ const o = ordinalFor(ctx.rootSchema, spVal)
4562
+ const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
4449
4563
  if (o !== null) lit._o = o
4450
4564
  ctx.closureVals.push(Object.freeze(lit))
4451
4565
  return `(_e||(_e=[])).push(${errVar})`
4566
+ }
4452
4567
  }
4453
4568
  }
4454
4569
  // Dynamic path (e.g., array index): inline as before
@@ -4511,15 +4626,22 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4511
4626
  destructKeys.push(`${JSON.stringify(key)}:${lv}`)
4512
4627
  }
4513
4628
  }
4514
- if (destructKeys.length > 0) lines.push(`const{${destructKeys.join(',')}}=${v}`)
4629
+ if (destructKeys.length > 0) {
4630
+ lines.push(`let{${destructKeys.join(',')}}=${v}`)
4631
+ // A value the object only inherits reads as absent, here and in the
4632
+ // property checks that use the same local below.
4633
+ for (const key of schema.required) {
4634
+ if (hoisted[key]) lines.push(`if(${hoisted[key]}!==undefined&&!${ownGuard(ctx, v, key)})${hoisted[key]}=undefined`)
4635
+ }
4636
+ }
4515
4637
  for (const key of schema.required) {
4516
- const check = hoisted[key] ? `${hoisted[key]}===undefined` : `${v}[${JSON.stringify(key)}]===undefined`
4638
+ const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
4517
4639
  if (isStaticPath) {
4518
4640
  const ei = ctx.varCounter++
4519
4641
  const errVar = `_E${ei}`
4520
- const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
4642
+ const pathVal = pathExpr ? literalValue(pathExpr) : ''
4521
4643
  ctx.closureVars.push(errVar)
4522
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
4644
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
4523
4645
  lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
4524
4646
  } else {
4525
4647
  lines.push(`if(${check}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
@@ -4531,12 +4653,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4531
4653
  if (isStatic) {
4532
4654
  const ei = ctx.varCounter++
4533
4655
  const errVar = `_E${ei}`
4534
- const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
4656
+ const pathVal = pathExpr ? literalValue(pathExpr) : ''
4535
4657
  ctx.closureVars.push(errVar)
4536
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required`, params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
4537
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
4658
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
4659
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
4538
4660
  } else {
4539
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!(${JSON.stringify(key)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
4661
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
4540
4662
  }
4541
4663
  }
4542
4664
  }
@@ -4696,12 +4818,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4696
4818
  if (isStatic) {
4697
4819
  const ei = ctx.varCounter++
4698
4820
  const errVar = `_E${ei}`
4699
- const pathVal = pathExpr ? pathExpr.slice(1, -1) : ''
4821
+ const pathVal = pathExpr ? literalValue(pathExpr) : ''
4700
4822
  ctx.closureVars.push(errVar)
4701
- ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/dependentRequired`, params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
4702
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push(${errVar})}`)
4823
+ ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
4824
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
4703
4825
  } else {
4704
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${JSON.stringify(key)} in ${v}&&!(${JSON.stringify(dep)} in ${v})){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
4826
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
4705
4827
  }
4706
4828
  }
4707
4829
  }
@@ -4713,16 +4835,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4713
4835
  const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
4714
4836
  const childPath = childPathExpr(pathExpr, ptrSeg(key))
4715
4837
  if (requiredSet.has(key) && isObj) {
4716
- lines.push(`if(${pv}!==undefined){`)
4838
+ // A hoisted local was already cleared if the value is only inherited.
4839
+ lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
4717
4840
  genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4718
4841
  lines.push(`}`)
4719
4842
  } else if (isObj) {
4720
4843
  const oi = ctx.varCounter++
4721
- lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
4844
+ lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
4722
4845
  genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4723
4846
  lines.push(`}}`)
4724
4847
  } else {
4725
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4848
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4726
4849
  genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
4727
4850
  lines.push(`}`)
4728
4851
  }
@@ -4838,7 +4961,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4838
4961
  // dependentSchemas
4839
4962
  if (schema.dependentSchemas) {
4840
4963
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
4841
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
4964
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
4842
4965
  genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
4843
4966
  lines.push(`}`)
4844
4967
  }
@@ -4904,7 +5027,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4904
5027
  if (schema.contains !== undefined) {
4905
5028
  const ci = ctx.varCounter++
4906
5029
  const subLines = []
4907
- genCode(schema.contains, `_cv`, subLines, ctx)
5030
+ nestedGenCode(schema.contains, `_cv`, subLines, ctx)
4908
5031
  const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
4909
5032
  const minC = schema.minContains !== undefined ? schema.minContains : 1
4910
5033
  const maxC = schema.maxContains
@@ -4920,20 +5043,20 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4920
5043
  // anyOf
4921
5044
  if (schema.anyOf) {
4922
5045
  const fi = ctx.varCounter++
4923
- const fns = schema.anyOf.map(sub => { const sl = []; genCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
5046
+ const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
4924
5047
  lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
4925
5048
  }
4926
5049
 
4927
5050
  // oneOf
4928
5051
  if (schema.oneOf) {
4929
5052
  const fi = ctx.varCounter++
4930
- const fns = schema.oneOf.map(sub => { const sl = []; genCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
5053
+ const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
4931
5054
  lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
4932
5055
  }
4933
5056
 
4934
5057
  // not
4935
5058
  if (schema.not !== undefined) {
4936
- const sl = []; genCode(schema.not, '_nv', sl, ctx)
5059
+ const sl = []; nestedGenCode(schema.not, '_nv', sl, ctx)
4937
5060
  const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
4938
5061
  const fi = ctx.varCounter++
4939
5062
  lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
@@ -4941,7 +5064,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4941
5064
 
4942
5065
  // if/then/else
4943
5066
  if (schema.if !== undefined) {
4944
- const sl = []; genCode(schema.if, '_iv', sl, ctx)
5067
+ const sl = []; nestedGenCode(schema.if, '_iv', sl, ctx)
4945
5068
  const fi = ctx.varCounter++
4946
5069
  const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
4947
5070
  lines.push(`{const _if${fi}=${ifFn}`)
@@ -69,6 +69,15 @@ function isTier0Object(schema) {
69
69
  for (const r of schema.required) if (typeof r !== 'string') return false;
70
70
  }
71
71
  const props = schema.properties;
72
+ // The tier-0 validator checks presence through the per-property constraints,
73
+ // so a required name with no entry in properties was never checked at all:
74
+ // `{ type: 'object', required: ['id'] }` accepted `{}` until the schema moved
75
+ // to generated code, and for good when the generator declined it.
76
+ if (schema.required !== undefined) {
77
+ for (const r of schema.required) {
78
+ if (props === undefined || props === null || typeof props !== 'object' || !Object.prototype.hasOwnProperty.call(props, r)) return false;
79
+ }
80
+ }
72
81
  if (props === undefined) return true;
73
82
  if (typeof props !== 'object' || props === null || Array.isArray(props)) return false;
74
83
  const keys = Object.keys(props);
package/lib/tier0.js CHANGED
@@ -41,6 +41,12 @@ const F_MULT = 16;
41
41
 
42
42
  // Build a constraint tuple for one primitive-typed property.
43
43
  // All fields have the same layout so every constraint shares one hidden class.
44
+ // Names found on Object.prototype. For these a read of data[key] finds the
45
+ // inherited value on every ordinary object, so presence is asked of the object
46
+ // itself; see ownKeyExpr in js-compiler.js.
47
+ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
48
+ const _hop = Object.prototype.hasOwnProperty;
49
+
44
50
  function primConstraint(key, propSchema) {
45
51
  const t = propSchema.type;
46
52
  const hasEnum = Array.isArray(propSchema.enum);
@@ -53,6 +59,7 @@ function primConstraint(key, propSchema) {
53
59
  if (typeof propSchema.multipleOf === 'number') numFlags |= F_MULT;
54
60
  return {
55
61
  key,
62
+ protoName: PROTO_NAMES.has(key),
56
63
  typeMask: TYPE_MASK[t] | 0,
57
64
  numFlags,
58
65
  hasEnum,
@@ -159,7 +166,10 @@ function tier0ValidateObject(plan, data) {
159
166
  let seenMask = 0;
160
167
  for (let i = 0; i < n; i++) {
161
168
  const c = cs[i];
162
- const v = data[c.key];
169
+ let v = data[c.key];
170
+ // Presence is an own-property question: a value the object inherits is
171
+ // treated as absent, as the interpreted engine treats it.
172
+ if (v !== undefined && (c.protoName || data.__proto__ !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
163
173
  if (v === undefined) {
164
174
  if (reqMask & (1 << i)) return false;
165
175
  continue;
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.31.0';
10
+ module.exports = '1.31.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.31.0",
3
+ "version": "1.31.2",
4
4
  "description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -51,7 +51,7 @@
51
51
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -121,13 +121,13 @@
121
121
  "LICENSE"
122
122
  ],
123
123
  "optionalDependencies": {
124
- "@ata-validator/native-darwin-arm64": "1.31.0",
125
- "@ata-validator/native-darwin-x64": "1.31.0",
126
- "@ata-validator/native-linux-arm64-gnu": "1.31.0",
127
- "@ata-validator/native-linux-arm64-musl": "1.31.0",
128
- "@ata-validator/native-linux-x64-gnu": "1.31.0",
129
- "@ata-validator/native-linux-x64-musl": "1.31.0",
130
- "@ata-validator/native-win32-x64": "1.31.0"
124
+ "@ata-validator/native-darwin-arm64": "1.31.2",
125
+ "@ata-validator/native-darwin-x64": "1.31.2",
126
+ "@ata-validator/native-linux-arm64-gnu": "1.31.2",
127
+ "@ata-validator/native-linux-arm64-musl": "1.31.2",
128
+ "@ata-validator/native-linux-x64-gnu": "1.31.2",
129
+ "@ata-validator/native-linux-x64-musl": "1.31.2",
130
+ "@ata-validator/native-win32-x64": "1.31.2"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "yaml": "^2.0.0"