ata-validator 0.15.1 → 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,19 @@
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
+
5
18
  ## 0.15.1 - 2026-05-23
6
19
 
7
20
  ### 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,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,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;
@@ -1074,6 +1083,7 @@ class Validator {
1074
1083
  }
1075
1084
  // Apply Draft 7 normalization if needed
1076
1085
  normalizeDraft7(schema)
1086
+ normalizeNullable(schema)
1077
1087
  this._schemaMap.set(schema.$id, schema)
1078
1088
  }
1079
1089
 
@@ -1642,6 +1652,14 @@ function attachSuggestions (errors, data) {
1642
1652
  return errors;
1643
1653
  }
1644
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
+
1645
1663
  module.exports = {
1646
1664
  Validator,
1647
1665
  compile,
@@ -1651,6 +1669,7 @@ module.exports = {
1651
1669
  SIMDJSON_PADDING,
1652
1670
  parseJSON,
1653
1671
  toTypeScript,
1672
+ defineSchema,
1654
1673
  renderPretty,
1655
1674
  renderCompact,
1656
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.1",
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",