ata-validator 0.15.1 → 0.17.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,29 @@
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.17.0 - 2026-05-23
6
+
7
+ ### Added
8
+
9
+ - Static type inference from JSON Schema literals. The new exported `Infer<S>` type maps a schema literal to its data type, and `new Validator(defineSchema({...}))` now returns `Validator<Infer<S>>`, so `validate()` narrows `result.data` with no manual type annotation. Write plain JSON Schema, get the type for free, no builder DSL. Covers primitives, type-array unions, `const`, `enum`, objects (required vs optional keys), and arrays; `$ref`, tuples, and `anyOf`/`oneOf` infer `unknown` for now. Pure `.d.ts` change, no runtime impact.
10
+
11
+ ### Fixed
12
+
13
+ - `validateAndParse()` is now implemented in JavaScript (`JSON.parse` then validate) and returns `{ valid, value, errors }`. It previously called a native method that does not exist and threw on every call. It now works with or without the native addon and in the browser; malformed JSON returns `valid: false` with an `ATA9001` error instead of throwing.
14
+
15
+ ## 0.16.0 - 2026-05-23
16
+
17
+ ### Added
18
+
19
+ - `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.
20
+ - OpenAPI `nullable` keyword. `{ type: 'string', nullable: true }` accepts `null` alongside the declared type, matching OpenAPI 3.0 schemas.
21
+
22
+ ### Fixed
23
+
24
+ - `coerceTypes` with `type: 'array'` wraps a scalar into a single-element array instead of leaving it unchanged.
25
+ - Codegen resolves a `$defs` entry that carries a fragment `$id` and is reached through a pointer `$ref`.
26
+ - Preprocessing (defaults, coercion, `removeAdditional`) guards against `null` and non-object data instead of throwing.
27
+
5
28
  ## 0.15.1 - 2026-05-23
6
29
 
7
30
  ### Fixed
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,163 @@ 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
+
167
+ /**
168
+ * Collapse an intersection of mapped object types into a single readable object
169
+ * type. `& {}` forces TypeScript to evaluate the mapped type eagerly.
170
+ */
171
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
172
+
173
+ /** Keys listed in a schema's `required` array, as a string union (or never). */
174
+ type RequiredKeys<S> = S extends { required: infer R }
175
+ ? R extends ReadonlyArray<infer K extends string>
176
+ ? K
177
+ : never
178
+ : never;
179
+
180
+ /** Object shape: required keys are required, all other declared keys optional. */
181
+ type InferObject<S> = S extends { properties: infer P }
182
+ ? Simplify<
183
+ { [K in keyof P as K extends RequiredKeys<S> ? K : never]: Infer<P[K]> } &
184
+ { [K in keyof P as K extends RequiredKeys<S> ? never : K]?: Infer<P[K]> }
185
+ >
186
+ : Record<string, unknown>;
187
+
188
+ /** Array shape: `items` as a single schema maps to an element type; tuple/absent -> unknown[]. */
189
+ type InferArray<S> = S extends { items: infer I }
190
+ ? I extends ReadonlyArray<unknown>
191
+ ? unknown[]
192
+ : Infer<I>[]
193
+ : unknown[];
194
+
195
+ /** Map a single JSON Schema type name (+ its schema) to a TS type. */
196
+ type InferByTypeName<N, S> = N extends 'object'
197
+ ? InferObject<S>
198
+ : N extends 'array'
199
+ ? InferArray<S>
200
+ : N extends 'string'
201
+ ? string
202
+ : N extends 'number'
203
+ ? number
204
+ : N extends 'integer'
205
+ ? number
206
+ : N extends 'boolean'
207
+ ? boolean
208
+ : N extends 'null'
209
+ ? null
210
+ : unknown;
211
+
212
+ /**
213
+ * Infer the TypeScript data type a JSON Schema literal describes (Core scope).
214
+ *
215
+ * Handles: primitives, `type` arrays (union), `const`, `enum`, objects
216
+ * (`properties` + `required` -> required/optional keys), and arrays
217
+ * (`items` as a single schema). `$ref`/`$defs`, tuples, and `anyOf`/`oneOf`/
218
+ * `allOf` are not yet inferred and resolve to `unknown` rather than erroring.
219
+ *
220
+ * Pair with {@link defineSchema}:
221
+ * `const s = defineSchema({...}); type T = Infer<typeof s>;`
222
+ */
223
+ export type Infer<S> = S extends { const: infer C }
224
+ ? C
225
+ : S extends { enum: infer E }
226
+ ? E extends ReadonlyArray<infer U>
227
+ ? U
228
+ : unknown
229
+ : S extends { type: infer T }
230
+ ? T extends ReadonlyArray<infer N>
231
+ ? InferByTypeName<N, S>
232
+ : InferByTypeName<T, S>
233
+ : unknown;
234
+
78
235
  export type ValidationResult<T = unknown> =
79
236
  | { valid: true; data: T; errors: ValidationError[] }
80
237
  | { valid: false; data?: never; errors: ValidationError[] };
@@ -142,9 +299,8 @@ export interface StandaloneModule {
142
299
  errFn: ((data: unknown, allErrors?: boolean) => ValidationResult) | null;
143
300
  }
144
301
 
145
- export class Validator<T = unknown> {
146
- constructor(schema: object | string, options?: ValidatorOptions);
147
-
302
+ /** Instance surface of a compiled validator. */
303
+ export interface Validator<T = unknown> {
148
304
  /** Add a schema to the registry for cross-schema $ref resolution */
149
305
  addSchema(schema: object): void;
150
306
 
@@ -160,7 +316,7 @@ export class Validator<T = unknown> {
160
316
  /** Fast boolean check for a JSON string */
161
317
  isValidJSON(jsonString: string): boolean;
162
318
 
163
- /** Parse JSON with simdjson + validate against schema. Returns parsed value and validation result. Requires native addon. */
319
+ /** Parse JSON and validate against the schema. Returns the parsed value and the validation result. Works without the native addon. */
164
320
  validateAndParse(jsonString: string | Buffer): ValidateAndParseResult<T>;
165
321
 
166
322
  /** Ultra-fast buffer validation via native addon */
@@ -187,41 +343,48 @@ export class Validator<T = unknown> {
187
343
  /**
188
344
  * Generate a self-contained module string with `validate`/`isValid` exports.
189
345
  * The output has zero runtime dependency on ata-validator.
190
- *
191
- * - format: 'esm' (default) or 'cjs'.
192
- * - abortEarly: if true, invalid results are a shared frozen stub (smaller output, no error details).
193
- *
194
- * Returns null if the schema cannot be compiled to a standalone module.
195
346
  */
196
347
  toStandaloneModule(options?: { format?: 'esm' | 'cjs'; abortEarly?: boolean }): string | null;
197
348
 
349
+ /** Standard Schema V1 interface, compatible with Fastify, tRPC, TanStack, etc. */
350
+ readonly "~standard": StandardSchemaV1Props;
351
+ }
352
+
353
+ /** Constructor + statics for {@link Validator}. */
354
+ export interface ValidatorConstructor {
355
+ /** Construct from a JSON Schema literal; the validated data type is inferred. */
356
+ new <const S extends JSONSchema>(schema: S, options?: ValidatorOptions): Validator<Infer<S>>;
357
+ /** Construct from a plain object/string schema, or with an explicit data type. */
358
+ new <T = unknown>(schema: object | string, options?: ValidatorOptions): Validator<T>;
359
+
198
360
  /** Load a pre-compiled standalone module. Zero schema compilation at startup. */
199
- static fromStandalone<T = unknown>(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator<T>;
361
+ fromStandalone<T = unknown>(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator<T>;
200
362
 
201
363
  /** Bundle multiple schemas into a single JS module string. Load with Validator.loadBundle(). */
202
- static bundle(schemas: object[], options?: ValidatorOptions): string;
364
+ bundle(schemas: object[], options?: ValidatorOptions): string;
203
365
 
204
366
  /**
205
367
  * Bundle multiple schemas into a self-contained JS module with no
206
368
  * ata-validator runtime dependency. Cross-schema `$ref` resolves between
207
369
  * the supplied schemas. Set `format: 'esm'` for ESM output (default 'cjs').
208
370
  */
209
- static bundleStandalone(schemas: object[], options?: BundleStandaloneOptions): string;
371
+ bundleStandalone(schemas: object[], options?: BundleStandaloneOptions): string;
210
372
 
211
373
  /**
212
374
  * Bundle multiple schemas with deduplicated shared templates. Smaller output
213
- * than bundle(). Accepts the same options as bundleStandalone, including
214
- * `format: 'esm' | 'cjs'` and cross-schema `$ref` resolution.
375
+ * than bundle(). Accepts the same options as bundleStandalone.
215
376
  */
216
- static bundleCompact(schemas: object[], options?: BundleStandaloneOptions): string;
377
+ bundleCompact(schemas: object[], options?: BundleStandaloneOptions): string;
217
378
 
218
379
  /** Load a bundle created by Validator.bundle(). Returns array of Validator instances. */
219
- static loadBundle(mods: object[], schemas: object[], options?: ValidatorOptions): Validator[];
380
+ loadBundle(mods: object[], schemas: object[], options?: ValidatorOptions): Validator[];
220
381
 
221
- /** Standard Schema V1 interface, compatible with Fastify, tRPC, TanStack, etc. */
222
- readonly "~standard": StandardSchemaV1Props;
382
+ readonly prototype: Validator;
223
383
  }
224
384
 
385
+ /** Compile a schema into a reusable validator. */
386
+ export const Validator: ValidatorConstructor;
387
+
225
388
  /** One-shot validate: creates a Validator, validates data, returns result. */
226
389
  export function validate<T = unknown>(
227
390
  schema: object | string,
@@ -253,3 +416,25 @@ export const SIMDJSON_PADDING: number;
253
416
  * build-time integrations (Vite plugin, custom build steps).
254
417
  */
255
418
  export function toTypeScript(schema: object, options?: { name?: string }): string;
419
+
420
+ /**
421
+ * Authoring helper for writing a JSON Schema inline in TypeScript. Returns the
422
+ * schema unchanged at runtime; its purpose is to apply the {@link JSONSchema}
423
+ * type so you get keyword autocomplete and an error on malformed values, while
424
+ * the `const` type parameter preserves the literal shape (no `as const` needed).
425
+ *
426
+ * ```ts
427
+ * import { defineSchema, Validator } from 'ata-validator';
428
+ *
429
+ * const user = defineSchema({
430
+ * type: 'object',
431
+ * properties: { id: { type: 'integer', minimum: 1 } },
432
+ * required: ['id'],
433
+ * });
434
+ *
435
+ * const v = new Validator(user);
436
+ * ```
437
+ *
438
+ * Requires TypeScript >= 5.0 for the `const` type parameter.
439
+ */
440
+ 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,6 +363,7 @@ 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
  }
@@ -459,6 +466,8 @@ class Validator {
459
466
 
460
467
  // Draft 7 normalization — convert keywords to 2020-12 equivalents in-place
461
468
  normalizeDraft7(schemaObj);
469
+ // OpenAPI nullable -> type union with 'null'
470
+ normalizeNullable(schemaObj);
462
471
 
463
472
  this._schemaStr = null; // lazy: computed on first use
464
473
  this._schemaObj = schemaObj;
@@ -859,16 +868,20 @@ class Validator {
859
868
  return false;
860
869
  }
861
870
  };
862
- // validateAndParse: requires native addon for simdjson parsing
863
- if (native) {
871
+ // validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
872
+ // validate) so it works with or without the native addon and in browsers.
873
+ {
864
874
  const self = this;
865
875
  this.validateAndParse = (jsonStr) => {
866
- self._ensureNative();
867
- self.validateAndParse = (s) => self._compiled.validateAndParse(s);
868
- return self.validateAndParse(jsonStr);
876
+ let value;
877
+ try {
878
+ value = JSON.parse(typeof jsonStr === 'string' ? jsonStr : new TextDecoder().decode(jsonStr));
879
+ } catch (e) {
880
+ return { valid: false, value: undefined, errors: [{ code: 'ATA9001', message: 'invalid JSON: ' + e.message, keyword: '__parse__', instancePath: '', schemaPath: '', params: {} }] };
881
+ }
882
+ const r = self.validate(value);
883
+ return { valid: r.valid, value, errors: r.errors };
869
884
  };
870
- } else {
871
- this.validateAndParse = () => { throw new Error('Native addon required for validateAndParse()'); };
872
885
  }
873
886
  // Buffer APIs: lazy native init — only compile native schema on first buffer call.
874
887
  // This keeps cold start fast (JS codegen only) for users who only use validate().
@@ -1074,6 +1087,7 @@ class Validator {
1074
1087
  }
1075
1088
  // Apply Draft 7 normalization if needed
1076
1089
  normalizeDraft7(schema)
1090
+ normalizeNullable(schema)
1077
1091
  this._schemaMap.set(schema.$id, schema)
1078
1092
  }
1079
1093
 
@@ -1642,6 +1656,14 @@ function attachSuggestions (errors, data) {
1642
1656
  return errors;
1643
1657
  }
1644
1658
 
1659
+ // Authoring helper: identity at runtime. Its only job is to attach the
1660
+ // JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
1661
+ // gives autocomplete and value checking while authoring. Returns the schema
1662
+ // untouched so it can be passed straight to Validator, toStandaloneModule, etc.
1663
+ function defineSchema (schema) {
1664
+ return schema;
1665
+ }
1666
+
1645
1667
  module.exports = {
1646
1668
  Validator,
1647
1669
  compile,
@@ -1651,6 +1673,7 @@ module.exports = {
1651
1673
  SIMDJSON_PADDING,
1652
1674
  parseJSON,
1653
1675
  toTypeScript,
1676
+ defineSchema,
1654
1677
  renderPretty,
1655
1678
  renderCompact,
1656
1679
  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.1",
3
+ "version": "0.17.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_validate_and_parse.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",