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 +23 -0
- package/README.md +22 -0
- package/index.d.ts +203 -18
- package/index.js +31 -8
- package/index.mjs +1 -1
- package/lib/draft7.js +48 -1
- package/lib/js-compiler.js +26 -1
- package/package.json +2 -2
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
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
* `format: 'esm' | 'cjs'` and cross-schema `$ref` resolution.
|
|
375
|
+
* than bundle(). Accepts the same options as bundleStandalone.
|
|
215
376
|
*/
|
|
216
|
-
|
|
377
|
+
bundleCompact(schemas: object[], options?: BundleStandaloneOptions): string;
|
|
217
378
|
|
|
218
379
|
/** Load a bundle created by Validator.bundle(). Returns array of Validator instances. */
|
|
219
|
-
|
|
380
|
+
loadBundle(mods: object[], schemas: object[], options?: ValidatorOptions): Validator[];
|
|
220
381
|
|
|
221
|
-
|
|
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:
|
|
863
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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.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",
|