ata-validator 0.15.0 → 0.16.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 0.16.0 - 2026-05-23
6
+
7
+ ### Added
8
+
9
+ - `defineSchema` helper and the exported `JSONSchema` type. Wrap a plain schema object in `defineSchema(...)` to author it inline in TypeScript with keyword autocomplete and value checking, no `as const` needed. It is an identity function at runtime, so the returned object drops straight into `Validator`, `toStandaloneModule`, and the rest of the API. Requires TypeScript >= 5.0 for the `const` type parameter.
10
+ - OpenAPI `nullable` keyword. `{ type: 'string', nullable: true }` accepts `null` alongside the declared type, matching OpenAPI 3.0 schemas.
11
+
12
+ ### Fixed
13
+
14
+ - `coerceTypes` with `type: 'array'` wraps a scalar into a single-element array instead of leaving it unchanged.
15
+ - Codegen resolves a `$defs` entry that carries a fragment `$id` and is reached through a pointer `$ref`.
16
+ - Preprocessing (defaults, coercion, `removeAdditional`) guards against `null` and non-object data instead of throwing.
17
+
18
+ ## 0.15.1 - 2026-05-23
19
+
20
+ ### Fixed
21
+
22
+ - Coercion, defaults, and `removeAdditional` now follow a cross-schema `$ref` to the referenced shape. A whole-schema reference like `{ $ref: 'shared#' }` (used for shared route schemas) or a property reference like `{ id: { $ref: 'shared#/properties/id' } }` is preprocessed instead of skipped.
23
+ - The compile cache now keys on referenced schema content, not just the `$id`. Two validators that share a root schema string and an `$id` pointing at different schemas no longer reuse the wrong compiled function.
24
+
5
25
  ## 0.15.0 - 2026-05-18
6
26
 
7
27
  ### Added
package/README.md CHANGED
@@ -158,6 +158,28 @@ if (result.valid) {
158
158
 
159
159
  The same pattern works with Zod-from-JSON-Schema, Valibot, or a hand-written `type User = {...}` alongside a JSON Schema literal. `Validator<T>` makes no library-specific assumption.
160
160
 
161
+ #### Authoring a schema inline: `defineSchema`
162
+
163
+ If you would rather write a plain JSON Schema object than reach for a schema library, wrap it in `defineSchema`. It returns the schema untouched at runtime, but in TypeScript it gives you keyword autocomplete and an error when a value has the wrong shape, with no `as const` needed.
164
+
165
+ ```ts
166
+ import { defineSchema, Validator } from 'ata-validator'
167
+
168
+ const userSchema = defineSchema({
169
+ type: 'object',
170
+ properties: {
171
+ id: { type: 'integer', minimum: 1 },
172
+ role: { type: 'string', enum: ['admin', 'user'] },
173
+ },
174
+ required: ['id'],
175
+ })
176
+
177
+ // type: 123 or required: 'id' would be a compile error here.
178
+ const v = new Validator(userSchema)
179
+ ```
180
+
181
+ The exported `JSONSchema` type is also available directly if you want to annotate a schema yourself. Custom and vendor keywords are allowed, so exotic schemas still type-check. Requires TypeScript >= 5.0.
182
+
161
183
  ### Cross-Schema `$ref`
162
184
 
163
185
  ```javascript
package/index.d.ts CHANGED
@@ -75,6 +75,95 @@ export function renderJSON(errors: RichValidationError[], opts?: JSONRenderOptio
75
75
  /** A user-supplied format checker. Receives the candidate value, returns true if valid. */
76
76
  export type FormatChecker = (value: string) => boolean;
77
77
 
78
+ /** The seven primitive `type` values defined by JSON Schema. */
79
+ export type JSONSchemaTypeName =
80
+ | 'string'
81
+ | 'number'
82
+ | 'integer'
83
+ | 'boolean'
84
+ | 'object'
85
+ | 'array'
86
+ | 'null';
87
+
88
+ /**
89
+ * A hand-written type for authoring JSON Schema (draft 2020-12) objects inline
90
+ * in TypeScript. Known keywords are typed, so you get autocomplete and an error
91
+ * when a value has the wrong shape (e.g. `type: 123` or `required: 'id'`).
92
+ * Unknown keywords are permitted, so custom/vendor keywords do not error.
93
+ *
94
+ * Pair it with {@link defineSchema} for inline authoring without `as const`.
95
+ */
96
+ export interface JSONSchema {
97
+ $id?: string;
98
+ $schema?: string;
99
+ $ref?: string;
100
+ $defs?: Record<string, JSONSchema>;
101
+ definitions?: Record<string, JSONSchema>;
102
+ $comment?: string;
103
+
104
+ type?: JSONSchemaTypeName | JSONSchemaTypeName[];
105
+ enum?: ReadonlyArray<unknown>;
106
+ const?: unknown;
107
+
108
+ title?: string;
109
+ description?: string;
110
+ default?: unknown;
111
+ examples?: ReadonlyArray<unknown>;
112
+ deprecated?: boolean;
113
+ readOnly?: boolean;
114
+ writeOnly?: boolean;
115
+
116
+ // object
117
+ properties?: Record<string, JSONSchema>;
118
+ required?: ReadonlyArray<string>;
119
+ additionalProperties?: boolean | JSONSchema;
120
+ patternProperties?: Record<string, JSONSchema>;
121
+ propertyNames?: JSONSchema;
122
+ minProperties?: number;
123
+ maxProperties?: number;
124
+ dependentRequired?: Record<string, ReadonlyArray<string>>;
125
+ dependentSchemas?: Record<string, JSONSchema>;
126
+
127
+ // array
128
+ items?: JSONSchema | ReadonlyArray<JSONSchema>;
129
+ prefixItems?: ReadonlyArray<JSONSchema>;
130
+ additionalItems?: boolean | JSONSchema;
131
+ contains?: JSONSchema;
132
+ minContains?: number;
133
+ maxContains?: number;
134
+ minItems?: number;
135
+ maxItems?: number;
136
+ uniqueItems?: boolean;
137
+
138
+ // string
139
+ minLength?: number;
140
+ maxLength?: number;
141
+ pattern?: string;
142
+ format?: string;
143
+
144
+ // number
145
+ minimum?: number;
146
+ maximum?: number;
147
+ exclusiveMinimum?: number;
148
+ exclusiveMaximum?: number;
149
+ multipleOf?: number;
150
+
151
+ // composition
152
+ allOf?: ReadonlyArray<JSONSchema>;
153
+ anyOf?: ReadonlyArray<JSONSchema>;
154
+ oneOf?: ReadonlyArray<JSONSchema>;
155
+ not?: JSONSchema;
156
+ if?: JSONSchema;
157
+ then?: JSONSchema;
158
+ else?: JSONSchema;
159
+
160
+ /** OpenAPI compatibility: ata honors `nullable` as a JSON Schema extension. */
161
+ nullable?: boolean;
162
+
163
+ /** Custom and vendor keywords are allowed without error. */
164
+ [keyword: string]: unknown;
165
+ }
166
+
78
167
  export type ValidationResult<T = unknown> =
79
168
  | { valid: true; data: T; errors: ValidationError[] }
80
169
  | { valid: false; data?: never; errors: ValidationError[] };
@@ -253,3 +342,25 @@ export const SIMDJSON_PADDING: number;
253
342
  * build-time integrations (Vite plugin, custom build steps).
254
343
  */
255
344
  export function toTypeScript(schema: object, options?: { name?: string }): string;
345
+
346
+ /**
347
+ * Authoring helper for writing a JSON Schema inline in TypeScript. Returns the
348
+ * schema unchanged at runtime; its purpose is to apply the {@link JSONSchema}
349
+ * type so you get keyword autocomplete and an error on malformed values, while
350
+ * the `const` type parameter preserves the literal shape (no `as const` needed).
351
+ *
352
+ * ```ts
353
+ * import { defineSchema, Validator } from 'ata-validator';
354
+ *
355
+ * const user = defineSchema({
356
+ * type: 'object',
357
+ * properties: { id: { type: 'integer', minimum: 1 } },
358
+ * required: ['id'],
359
+ * });
360
+ *
361
+ * const v = new Validator(user);
362
+ * ```
363
+ *
364
+ * Requires TypeScript >= 5.0 for the `const` type parameter.
365
+ */
366
+ export declare function defineSchema<const S extends JSONSchema>(schema: S): S;
package/index.js CHANGED
@@ -8,7 +8,7 @@ const {
8
8
  compileToJSCodegenWithErrors,
9
9
  compileToJSCombined,
10
10
  } = require("./lib/js-compiler");
11
- const { normalizeDraft7 } = require("./lib/draft7");
11
+ const { normalizeDraft7, normalizeNullable } = require("./lib/draft7");
12
12
  const { classify } = require("./lib/shape-classifier");
13
13
  const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
14
14
 
@@ -242,6 +242,8 @@ function buildPreprocessCodegen(schema, options) {
242
242
  } else if (t === 'boolean') {
243
243
  lines.push(`if(d[${k}]==='true'||d[${k}]==='1')d[${k}]=true`);
244
244
  lines.push(`if(d[${k}]==='false'||d[${k}]==='0')d[${k}]=false`);
245
+ } else if (t === 'array' && options.coerceTypes === 'array') {
246
+ lines.push(`if(${k} in d&&d[${k}]!==undefined&&!Array.isArray(d[${k}]))d[${k}]=[d[${k}]]`);
245
247
  }
246
248
  }
247
249
  }
@@ -256,6 +258,9 @@ function buildPreprocessCodegen(schema, options) {
256
258
  }
257
259
 
258
260
  if (lines.length === 0) return null;
261
+ // Data may legitimately be null or a non-object (e.g. a `['object','null']`
262
+ // schema), so the per-property mutations must not run on it.
263
+ lines.unshift(`if(d===null||typeof d!=='object')return`);
259
264
  try {
260
265
  return new Function('d', lines.join('\n'));
261
266
  } catch {
@@ -350,6 +355,7 @@ function buildSchemaMap(schemas) {
350
355
  if (Array.isArray(schemas)) {
351
356
  for (const s of schemas) {
352
357
  normalizeDraft7(s)
358
+ normalizeNullable(s)
353
359
  const id = s.$id
354
360
  if (!id) throw new Error('Schema in schemas option must have $id')
355
361
  map.set(id, s)
@@ -357,12 +363,26 @@ function buildSchemaMap(schemas) {
357
363
  } else {
358
364
  for (const [key, s] of Object.entries(schemas)) {
359
365
  normalizeDraft7(s)
366
+ normalizeNullable(s)
360
367
  map.set(s.$id || key, s)
361
368
  }
362
369
  }
363
370
  return map
364
371
  }
365
372
 
373
+ // Compile-cache key for a root schema plus its external schemas. Must include
374
+ // the external schema CONTENT, not just their $ids: two validators can share a
375
+ // root schema string and the same $id while pointing that $id at different
376
+ // schemas (separate app instances, test suites, multi-tenant). Keying on $id
377
+ // alone reuses the wrong compiled validator and silently mis-validates.
378
+ function compileCacheKey(schemaStr, schemaMap) {
379
+ if (!schemaMap || schemaMap.size === 0) return schemaStr
380
+ const parts = []
381
+ for (const [id, s] of schemaMap) parts.push(id + '=' + JSON.stringify(s))
382
+ parts.sort()
383
+ return schemaStr + '\0' + parts.join('\0')
384
+ }
385
+
366
386
  // Resolve a relative URI ref against a base URI
367
387
  function resolveRelativeRef(ref, baseId) {
368
388
  if (!baseId || ref.includes('://') || ref.startsWith('#')) return ref
@@ -371,6 +391,66 @@ function resolveRelativeRef(ref, baseId) {
371
391
  return baseId.substring(0, lastSlash + 1) + ref
372
392
  }
373
393
 
394
+ // Resolve a cross-schema $ref to its target schema for preprocessing purposes.
395
+ // Handles whole-schema refs (`shared#`), relative-id matching, and JSON pointer
396
+ // fragments (`shared#/properties/id`). Returns null for local-only refs or when
397
+ // the target cannot be found. Used only to read `type`/`properties` for
398
+ // coercion/defaults/removeAdditional, never for validation.
399
+ function resolveRefForPreprocess(ref, schemaMap) {
400
+ if (!schemaMap || schemaMap.size === 0 || typeof ref !== 'string') return null
401
+ const hashIdx = ref.indexOf('#')
402
+ const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
403
+ const fragment = hashIdx >= 0 ? ref.slice(hashIdx + 1) : ''
404
+ if (!baseId) return null
405
+ let base = null
406
+ if (schemaMap.has(baseId)) base = schemaMap.get(baseId)
407
+ else if (!ref.includes('://')) {
408
+ for (const [id, s] of schemaMap) {
409
+ if (id.endsWith('/' + baseId)) { base = s; break }
410
+ }
411
+ }
412
+ if (!base) return null
413
+ if (!fragment) return base
414
+ let target = base
415
+ for (const part of fragment.split('/')) {
416
+ if (part === '') continue
417
+ if (target == null || typeof target !== 'object') return null
418
+ target = target[part.replace(/~1/g, '/').replace(/~0/g, '~')]
419
+ }
420
+ return target == null ? null : target
421
+ }
422
+
423
+ // Preprocessing (coerce/defaults/removeAdditional) reads `schema.properties` and
424
+ // each property's `type`. When the data shape lives behind a cross-schema $ref
425
+ // (a whole-schema ref like Fastify's `params: { $ref: 'shared#' }`, or a
426
+ // property ref like `{ id: { $ref: 'shared#/properties/id' } }`), follow the
427
+ // ref so the preprocessor can see the referenced shape. Returns the schema with
428
+ // such refs resolved, cloning only when a substitution is made.
429
+ function resolveSchemaForPreprocess(schema, schemaMap) {
430
+ if (!schema || typeof schema !== 'object' || !schemaMap || schemaMap.size === 0) return schema
431
+ let s = schema
432
+ // Whole-schema ref (only when it has no own properties, to avoid dropping
433
+ // sibling keywords on schemas that mix $ref with properties).
434
+ if (s.$ref && !s.properties) {
435
+ const t = resolveRefForPreprocess(s.$ref, schemaMap)
436
+ if (t && typeof t === 'object') s = t
437
+ }
438
+ if (!s.properties) return s
439
+ // Property-level refs: substitute the resolved target so coercion sees `type`.
440
+ let cloned = null
441
+ for (const key of Object.keys(s.properties)) {
442
+ const p = s.properties[key]
443
+ if (p && typeof p === 'object' && p.$ref && !p.type) {
444
+ const t = resolveRefForPreprocess(p.$ref, schemaMap)
445
+ if (t && typeof t === 'object') {
446
+ if (!cloned) { cloned = Object.assign({}, s); cloned.properties = Object.assign({}, s.properties) }
447
+ cloned.properties[key] = t
448
+ }
449
+ }
450
+ }
451
+ return cloned || s
452
+ }
453
+
374
454
  class Validator {
375
455
  constructor(schema, opts) {
376
456
  const options = opts || {};
@@ -386,6 +466,8 @@ class Validator {
386
466
 
387
467
  // Draft 7 normalization — convert keywords to 2020-12 equivalents in-place
388
468
  normalizeDraft7(schemaObj);
469
+ // OpenAPI nullable -> type union with 'null'
470
+ normalizeNullable(schemaObj);
389
471
 
390
472
  this._schemaStr = null; // lazy: computed on first use
391
473
  this._schemaObj = schemaObj;
@@ -531,9 +613,7 @@ class Validator {
531
613
 
532
614
  // Check cache first -- reuse compiled functions for same schema
533
615
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
534
- const mapKey = this._schemaMap.size > 0
535
- ? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
536
- : this._schemaStr;
616
+ const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
537
617
  // Custom formats are JS functions: bypass the compile cache since they can
538
618
  // differ between validators that share the same schema string.
539
619
  const cached = this._userFormats ? null : _compileCache.get(mapKey);
@@ -559,13 +639,17 @@ class Validator {
559
639
  }
560
640
  this._jsFn = jsFn;
561
641
 
562
- // Data mutators -- try codegen first (12x faster), fallback to closure arrays
563
- let preprocess = buildPreprocessCodegen(schemaObj, options);
642
+ // Data mutators -- try codegen first (12x faster), fallback to closure arrays.
643
+ // Follow cross-refs so coercion/defaults/removeAdditional see the referenced
644
+ // shape (e.g. Fastify `params: { $ref: 'shared#' }` or property refs like
645
+ // `{ id: { $ref: 'shared#/properties/id' } }`).
646
+ const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
647
+ let preprocess = buildPreprocessCodegen(preprocessSchema, options);
564
648
  if (!preprocess) {
565
- const applyDefaults = buildDefaultsApplier(schemaObj);
566
- const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
649
+ const applyDefaults = buildDefaultsApplier(preprocessSchema);
650
+ const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
567
651
  const applyRemove = options.removeAdditional
568
- ? buildRemover(schemaObj)
652
+ ? buildRemover(preprocessSchema)
569
653
  : null;
570
654
  const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
571
655
  preprocess =
@@ -999,6 +1083,7 @@ class Validator {
999
1083
  }
1000
1084
  // Apply Draft 7 normalization if needed
1001
1085
  normalizeDraft7(schema)
1086
+ normalizeNullable(schema)
1002
1087
  this._schemaMap.set(schema.$id, schema)
1003
1088
  }
1004
1089
 
@@ -1007,9 +1092,7 @@ class Validator {
1007
1092
  if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
1008
1093
  if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
1009
1094
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
1010
- const mapKey = this._schemaMap.size > 0
1011
- ? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
1012
- : this._schemaStr;
1095
+ const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
1013
1096
  // Custom formats are JS functions: skip the shared cache so different
1014
1097
  // validators with the same schema string but different formats don't collide.
1015
1098
  const cached = this._userFormats ? null : _compileCache.get(mapKey);
@@ -1569,6 +1652,14 @@ function attachSuggestions (errors, data) {
1569
1652
  return errors;
1570
1653
  }
1571
1654
 
1655
+ // Authoring helper: identity at runtime. Its only job is to attach the
1656
+ // JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
1657
+ // gives autocomplete and value checking while authoring. Returns the schema
1658
+ // untouched so it can be passed straight to Validator, toStandaloneModule, etc.
1659
+ function defineSchema (schema) {
1660
+ return schema;
1661
+ }
1662
+
1572
1663
  module.exports = {
1573
1664
  Validator,
1574
1665
  compile,
@@ -1578,6 +1669,7 @@ module.exports = {
1578
1669
  SIMDJSON_PADDING,
1579
1670
  parseJSON,
1580
1671
  toTypeScript,
1672
+ defineSchema,
1581
1673
  renderPretty,
1582
1674
  renderCompact,
1583
1675
  renderJSON,
package/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './index.js';
2
- export const { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING, renderPretty, renderCompact, renderJSON } = mod;
2
+ export const { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING, defineSchema, renderPretty, renderCompact, renderJSON } = mod;
3
3
  export default mod;
package/lib/draft7.js CHANGED
@@ -79,4 +79,51 @@ function _normalize(schema) {
79
79
  }
80
80
  }
81
81
 
82
- module.exports = { isDraft7, normalizeDraft7 }
82
+ // OpenAPI `nullable: true` is not JSON Schema. Convert it to a union with
83
+ // 'null' (`{ type: 'X', nullable: true }` -> `{ type: ['X', 'null'] }`), the
84
+ // same shape AJV produces under its `nullable` option. Recurses only through
85
+ // schema-bearing keywords so it never touches data values (default, const, etc.).
86
+ function normalizeNullable(schema) {
87
+ if (typeof schema !== 'object' || schema === null) return schema
88
+ _normalizeNullable(schema)
89
+ return schema
90
+ }
91
+
92
+ function _normalizeNullable(schema) {
93
+ if (typeof schema !== 'object' || schema === null) return
94
+
95
+ if (schema.nullable === true && schema.type !== undefined) {
96
+ if (Array.isArray(schema.type)) {
97
+ if (!schema.type.includes('null')) schema.type = schema.type.concat('null')
98
+ } else {
99
+ schema.type = [schema.type, 'null']
100
+ }
101
+ }
102
+ if ('nullable' in schema) delete schema.nullable
103
+
104
+ const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
105
+ for (const key of objSubs) {
106
+ if (schema[key] && typeof schema[key] === 'object') {
107
+ for (const v of Object.values(schema[key])) {
108
+ if (typeof v === 'object' && v !== null) _normalizeNullable(v)
109
+ }
110
+ }
111
+ }
112
+ const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
113
+ for (const key of arrSubs) {
114
+ if (Array.isArray(schema[key])) {
115
+ for (const s of schema[key]) {
116
+ if (typeof s === 'object' && s !== null) _normalizeNullable(s)
117
+ }
118
+ }
119
+ }
120
+ const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else',
121
+ 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties']
122
+ for (const key of singleSubs) {
123
+ if (typeof schema[key] === 'object' && schema[key] !== null) {
124
+ _normalizeNullable(schema[key])
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports = { isDraft7, normalizeDraft7, normalizeNullable }
@@ -690,6 +690,23 @@ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
690
690
 
691
691
  // Recursively check if a schema can be safely compiled to JS codegen.
692
692
  // Returns false if any sub-schema contains features codegen gets wrong.
693
+ // Does the schema (recursively) contain an anchor-style `$ref` equal to anchorId
694
+ // (e.g. `$ref: '#address'`)? Used to keep codegen away from anchor refs it can't
695
+ // resolve in the combined/error paths.
696
+ function schemaHasAnchorRef(node, anchorId) {
697
+ if (!node || typeof node !== 'object') return false
698
+ if (Array.isArray(node)) {
699
+ for (const x of node) if (schemaHasAnchorRef(x, anchorId)) return true
700
+ return false
701
+ }
702
+ if (node.$ref === anchorId) return true
703
+ for (const k in node) {
704
+ if (k === '$ref') continue
705
+ if (schemaHasAnchorRef(node[k], anchorId)) return true
706
+ }
707
+ return false
708
+ }
709
+
693
710
  function codegenSafe(schema, schemaMap) {
694
711
  if (typeof schema === 'boolean') return true
695
712
  if (typeof schema !== 'object' || schema === null) return true
@@ -799,7 +816,15 @@ function codegenSafe(schema, schemaMap) {
799
816
  if (/[~/"']/.test(name)) return false // special chars in def name
800
817
  if (typeof def === 'boolean') return false
801
818
  if (typeof def === 'object' && def !== null) {
802
- if (def.$id) return false // $id in $defs creates new resolution scope — bail
819
+ // A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
820
+ // scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; it is
821
+ // safe to codegen when reached by JSON pointer (#/definitions/name), but the
822
+ // combined/error codegen paths do not resolve anchor refs (`$ref: '#name'`)
823
+ // to it, so bail if the schema anchor-references this def.
824
+ if (def.$id) {
825
+ if (!def.$id.startsWith('#')) return false
826
+ if (schemaHasAnchorRef(schema, def.$id)) return false
827
+ }
803
828
  if (def.$ref) return false // nested ref chain — bail
804
829
  if (!codegenSafe(def, schemaMap)) return false
805
830
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -42,7 +42,7 @@
42
42
  "rebuild": "cmake-js rebuild --target ata",
43
43
  "prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
44
44
  "prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
45
- "test": "node test.js && node tests/test_no_native.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_typed_validator_runner.js && node tests/test_error_codes_lock.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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 benchmark/bench_aot_size.mjs",
45
+ "test": "node test.js && node tests/test_no_native.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_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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 benchmark/bench_aot_size.mjs",
46
46
  "bench:size": "node benchmark/bench_aot_size.mjs",
47
47
  "test:suite": "node tests/run_suite.js",
48
48
  "test:compat": "node tests/test_compat.js",
@@ -10,7 +10,7 @@ const doc = fs.readFileSync(path.join(__dirname, '..', 'docs', 'error-codes.md')
10
10
  const missing = [];
11
11
  const placeholder = [];
12
12
  for (const code of all()) {
13
- const headingRe = new RegExp(`^### ${code} `, 'm');
13
+ const headingRe = new RegExp(`^### ${code}\\b`, 'm');
14
14
  if (!headingRe.test(doc)) {
15
15
  missing.push(code);
16
16
  continue;