ata-validator 0.17.4 → 0.18.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,18 @@
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.18.0 - 2026-05-25
6
+
7
+ ### Added
8
+
9
+ - `Infer<S>` resolves the shapes 0.17.0 left as `unknown`. `anyOf` and `oneOf` map to unions, `allOf` to an intersection, `prefixItems` to a tuple, and a `$ref` to a local `#/$defs/...` or `#/definitions/...` entry resolves to the referenced type, including recursive references. An external or otherwise unresolvable `$ref` still resolves to `unknown` rather than erroring. `new Validator(schema)` carries the wider inference, so handlers narrow `result.data` for these schemas with no manual annotation, and the same applies to the Fastify type provider that builds on `Infer`. Pure `.d.ts` change, no runtime impact.
10
+
11
+ ## 0.17.5 - 2026-05-25
12
+
13
+ ### Fixed
14
+
15
+ - Compiled validators resolve draft-07 plain-name anchors. A `$defs`/`definitions` entry that declares an anchor with `$id: "#name"` and is referenced by `$ref: "#name"` now compiles through the codegen on every path (boolean, error, combined) instead of bailing. The bail forced a fallback that could not resolve sibling cross-schema refs, which surfaced as `cannot resolve $ref`. This is how shared schemas are referenced under Fastify.
16
+
5
17
  ## 0.17.4 - 2026-05-25
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -180,6 +180,36 @@ const v = new Validator(userSchema)
180
180
 
181
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
182
 
183
+ #### Extracting the type: `Infer`
184
+
185
+ Because `defineSchema` leaves the schema as a plain object, you can pull a TypeScript type straight out of it with `Infer`, with no second type declaration to keep in sync.
186
+
187
+ ```ts
188
+ import { defineSchema, type Infer } from 'ata-validator'
189
+
190
+ const event = defineSchema({
191
+ $defs: {
192
+ Point: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] },
193
+ },
194
+ type: 'object',
195
+ properties: {
196
+ kind: { enum: ['click', 'scroll'] },
197
+ at: { $ref: '#/$defs/Point' },
198
+ path: { type: 'array', prefixItems: [{ type: 'string' }, { type: 'integer' }] },
199
+ },
200
+ required: ['kind', 'at'],
201
+ })
202
+
203
+ type Event = Infer<typeof event>
204
+ // {
205
+ // kind: 'click' | 'scroll'
206
+ // at: { x: number; y: number }
207
+ // path?: [string, number]
208
+ // }
209
+ ```
210
+
211
+ `Infer` resolves `const`/`enum` to literals, `anyOf`/`oneOf` to unions, `allOf` to intersections, `prefixItems` to tuples, and local `$ref` into `#/$defs` or `#/definitions`, including recursive references. `new Validator(schema)` carries the same type, so a successful `validate` narrows `result.data` without a manual annotation. An external or unresolvable `$ref` resolves to `unknown` rather than erroring.
212
+
183
213
  ### Cross-Schema `$ref`
184
214
 
185
215
  ```javascript
package/index.d.ts CHANGED
@@ -177,26 +177,55 @@ type RequiredKeys<S> = S extends { required: infer R }
177
177
  : never
178
178
  : never;
179
179
 
180
+ /** Collapse a union of types into their intersection (used for `allOf`). */
181
+ type UnionToIntersection<U> =
182
+ (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
183
+
184
+ /** The root `$defs`/`definitions` map, threaded through inference for `$ref` resolution. */
185
+ type RootDefs<S> = S extends { $defs: infer D }
186
+ ? D
187
+ : S extends { definitions: infer D }
188
+ ? D
189
+ : {};
190
+
191
+ /** Extract the definition name from a local `#/$defs/...` or `#/definitions/...` pointer. */
192
+ type RefName<R> = R extends `#/$defs/${infer N}`
193
+ ? N
194
+ : R extends `#/definitions/${infer N}`
195
+ ? N
196
+ : never;
197
+
198
+ /** Resolve a `$ref` against the root defs map; external/unresolvable refs -> unknown. */
199
+ type ResolveRef<R, D> = [RefName<R>] extends [never]
200
+ ? unknown
201
+ : RefName<R> extends keyof D
202
+ ? InferWith<D[RefName<R>], D>
203
+ : unknown;
204
+
180
205
  /** Object shape: required keys are required, all other declared keys optional. */
181
- type InferObject<S> = S extends { properties: infer P }
206
+ type InferObject<S, D> = S extends { properties: infer P }
182
207
  ? 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]> }
208
+ { [K in keyof P as K extends RequiredKeys<S> ? K : never]: InferWith<P[K], D> } &
209
+ { [K in keyof P as K extends RequiredKeys<S> ? never : K]?: InferWith<P[K], D> }
185
210
  >
186
211
  : Record<string, unknown>;
187
212
 
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[];
213
+ /** Array shape: `prefixItems` -> tuple; `items` (single schema) -> element type; otherwise unknown[]. */
214
+ type InferArray<S, D> = S extends { prefixItems: infer P }
215
+ ? P extends ReadonlyArray<unknown>
216
+ ? { -readonly [K in keyof P]: InferWith<P[K], D> }
217
+ : unknown[]
218
+ : S extends { items: infer I }
219
+ ? I extends ReadonlyArray<unknown>
220
+ ? unknown[]
221
+ : InferWith<I, D>[]
222
+ : unknown[];
194
223
 
195
224
  /** Map a single JSON Schema type name (+ its schema) to a TS type. */
196
- type InferByTypeName<N, S> = N extends 'object'
197
- ? InferObject<S>
225
+ type InferByTypeName<N, S, D> = N extends 'object'
226
+ ? InferObject<S, D>
198
227
  : N extends 'array'
199
- ? InferArray<S>
228
+ ? InferArray<S, D>
200
229
  : N extends 'string'
201
230
  ? string
202
231
  : N extends 'number'
@@ -209,28 +238,46 @@ type InferByTypeName<N, S> = N extends 'object'
209
238
  ? null
210
239
  : unknown;
211
240
 
241
+ /** Core inference with the root defs map `D` threaded for `$ref` resolution. */
242
+ type InferWith<S, D> = S extends { $ref: infer R }
243
+ ? ResolveRef<R, D>
244
+ : S extends { const: infer C }
245
+ ? C
246
+ : S extends { enum: infer E }
247
+ ? E extends ReadonlyArray<infer U>
248
+ ? U
249
+ : unknown
250
+ : S extends { allOf: infer A }
251
+ ? A extends ReadonlyArray<unknown>
252
+ ? Simplify<UnionToIntersection<{ [K in keyof A]: InferWith<A[K], D> }[number]>>
253
+ : unknown
254
+ : S extends { anyOf: infer A }
255
+ ? A extends ReadonlyArray<unknown>
256
+ ? { [K in keyof A]: InferWith<A[K], D> }[number]
257
+ : unknown
258
+ : S extends { oneOf: infer A }
259
+ ? A extends ReadonlyArray<unknown>
260
+ ? { [K in keyof A]: InferWith<A[K], D> }[number]
261
+ : unknown
262
+ : S extends { type: infer T }
263
+ ? T extends ReadonlyArray<infer N>
264
+ ? InferByTypeName<N, S, D>
265
+ : InferByTypeName<T, S, D>
266
+ : unknown;
267
+
212
268
  /**
213
- * Infer the TypeScript data type a JSON Schema literal describes (Core scope).
269
+ * Infer the TypeScript data type a JSON Schema literal describes.
214
270
  *
215
271
  * 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.
272
+ * (`properties` + `required` -> required/optional keys), arrays (`items`),
273
+ * tuples (`prefixItems`), `anyOf`/`oneOf` (union), `allOf` (intersection),
274
+ * and `$ref` to local `#/$defs/...` or `#/definitions/...`. External or
275
+ * unresolvable `$ref` resolves to `unknown` rather than erroring.
219
276
  *
220
277
  * Pair with {@link defineSchema}:
221
278
  * `const s = defineSchema({...}); type T = Infer<typeof s>;`
222
279
  */
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;
280
+ export type Infer<S> = InferWith<S, RootDefs<S>>;
234
281
 
235
282
  export type ValidationResult<T = unknown> =
236
283
  | { valid: true; data: T; errors: ValidationError[] }
@@ -699,23 +699,6 @@ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
699
699
 
700
700
  // Recursively check if a schema can be safely compiled to JS codegen.
701
701
  // Returns false if any sub-schema contains features codegen gets wrong.
702
- // Does the schema (recursively) contain an anchor-style `$ref` equal to anchorId
703
- // (e.g. `$ref: '#address'`)? Used to keep codegen away from anchor refs it can't
704
- // resolve in the combined/error paths.
705
- function schemaHasAnchorRef(node, anchorId) {
706
- if (!node || typeof node !== 'object') return false
707
- if (Array.isArray(node)) {
708
- for (const x of node) if (schemaHasAnchorRef(x, anchorId)) return true
709
- return false
710
- }
711
- if (node.$ref === anchorId) return true
712
- for (const k in node) {
713
- if (k === '$ref') continue
714
- if (schemaHasAnchorRef(node[k], anchorId)) return true
715
- }
716
- return false
717
- }
718
-
719
702
  function codegenSafe(schema, schemaMap) {
720
703
  if (typeof schema === 'boolean') return true
721
704
  if (typeof schema !== 'object' || schema === null) return true
@@ -826,14 +809,10 @@ function codegenSafe(schema, schemaMap) {
826
809
  if (typeof def === 'boolean') return false
827
810
  if (typeof def === 'object' && def !== null) {
828
811
  // A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
829
- // scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; it is
830
- // safe to codegen when reached by JSON pointer (#/definitions/name), but the
831
- // combined/error codegen paths do not resolve anchor refs (`$ref: '#name'`)
832
- // to it, so bail if the schema anchor-references this def.
833
- if (def.$id) {
834
- if (!def.$id.startsWith('#')) return false
835
- if (schemaHasAnchorRef(schema, def.$id)) return false
836
- }
812
+ // scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; the
813
+ // codegen anchor maps register it (see "Build anchors map"), so anchor
814
+ // refs (`$ref: '#name'`) resolve to it on all paths.
815
+ if (def.$id && !def.$id.startsWith('#')) return false
837
816
  if (def.$ref) return false // nested ref chain — bail
838
817
  if (!codegenSafe(def, schemaMap)) return false
839
818
  }
@@ -933,12 +912,15 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
933
912
  // Root schema's own $dynamicAnchor / $anchor
934
913
  if (schema.$dynamicAnchor) anchors['#' + schema.$dynamicAnchor] = schema
935
914
  if (schema.$anchor) anchors['#' + schema.$anchor] = schema
915
+ // Draft-07 plain-name anchor: declared as `$id: "#name"`.
916
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) anchors[schema.$id] = schema
936
917
  // Anchors from $defs
937
918
  if (rootDefs) {
938
919
  for (const def of Object.values(rootDefs)) {
939
920
  if (def && typeof def === 'object') {
940
921
  if (def.$dynamicAnchor) anchors['#' + def.$dynamicAnchor] = def
941
922
  if (def.$anchor) anchors['#' + def.$anchor] = def
923
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) anchors[def.$id] = def
942
924
  }
943
925
  }
944
926
  }
@@ -948,6 +930,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
948
930
  if (ext && typeof ext === 'object') {
949
931
  if (ext.$dynamicAnchor && !anchors['#' + ext.$dynamicAnchor]) anchors['#' + ext.$dynamicAnchor] = ext
950
932
  if (ext.$anchor && !anchors['#' + ext.$anchor]) anchors['#' + ext.$anchor] = ext
933
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !anchors[ext.$id]) anchors[ext.$id] = ext
951
934
  }
952
935
  }
953
936
  }
@@ -2592,11 +2575,13 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
2592
2575
  const eAnchors = {}
2593
2576
  if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
2594
2577
  if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
2578
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) eAnchors[schema.$id] = schema
2595
2579
  if (eRootDefs) {
2596
2580
  for (const def of Object.values(eRootDefs)) {
2597
2581
  if (def && typeof def === 'object') {
2598
2582
  if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
2599
2583
  if (def.$anchor) eAnchors['#' + def.$anchor] = def
2584
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) eAnchors[def.$id] = def
2600
2585
  }
2601
2586
  }
2602
2587
  }
@@ -2605,6 +2590,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
2605
2590
  if (ext && typeof ext === 'object') {
2606
2591
  if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
2607
2592
  if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
2593
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !eAnchors[ext.$id]) eAnchors[ext.$id] = ext
2608
2594
  }
2609
2595
  }
2610
2596
  }
@@ -3210,11 +3196,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3210
3196
  const cAnchors = {}
3211
3197
  if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
3212
3198
  if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
3199
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) cAnchors[schema.$id] = schema
3213
3200
  if (cRootDefs) {
3214
3201
  for (const def of Object.values(cRootDefs)) {
3215
3202
  if (def && typeof def === 'object') {
3216
3203
  if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
3217
3204
  if (def.$anchor) cAnchors['#' + def.$anchor] = def
3205
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) cAnchors[def.$id] = def
3218
3206
  }
3219
3207
  }
3220
3208
  }
@@ -3223,6 +3211,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3223
3211
  if (ext && typeof ext === 'object') {
3224
3212
  if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
3225
3213
  if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
3214
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !cAnchors[ext.$id]) cAnchors[ext.$id] = ext
3226
3215
  }
3227
3216
  }
3228
3217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.17.4",
3
+ "version": "0.18.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_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_additional_props_errors.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_validate_data.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 tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
45
+ "test": "node test.js && node tests/test_no_native.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_additional_props_errors.js && node tests/test_id_anchor_refs.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_validate_data.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 tests/test_cli_version.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",