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 +20 -0
- package/README.md +22 -0
- package/index.d.ts +111 -0
- package/index.js +104 -12
- package/index.mjs +1 -1
- package/lib/draft7.js +48 -1
- package/lib/js-compiler.js +26 -1
- package/package.json +2 -2
- package/scripts/check-doc-coverage.js +1 -1
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
|
|
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
|
-
|
|
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(
|
|
566
|
-
const applyCoerce = options.coerceTypes ? buildCoercer(
|
|
649
|
+
const applyDefaults = buildDefaultsApplier(preprocessSchema);
|
|
650
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
|
|
567
651
|
const applyRemove = options.removeAdditional
|
|
568
|
-
? buildRemover(
|
|
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
|
|
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
|
-
|
|
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 }
|
package/lib/js-compiler.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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}
|
|
13
|
+
const headingRe = new RegExp(`^### ${code}\\b`, 'm');
|
|
14
14
|
if (!headingRe.test(doc)) {
|
|
15
15
|
missing.push(code);
|
|
16
16
|
continue;
|