@amritk/generate-parsers 0.16.3 → 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.
@@ -1,1340 +1,909 @@
1
- import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
- import { quoteJsString } from '@amritk/helpers/quote-js-string';
3
- import { refToName } from '@amritk/helpers/ref-to-name';
4
- import { resolveRef } from '@amritk/helpers/resolve-ref';
5
- import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasRequired, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
- import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
8
- import { findDiscriminator } from '../helpers/find-discriminator.js';
9
- import { getDefaultValue } from '../helpers/get-default-value.js';
10
- import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
11
- import { assertNoUnsupportedKeywords } from './assert-supported-keywords.js';
12
- import { generateEnumCaseInsensitiveCoercion } from './generate-enum-check.js';
13
- import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, } from './generate-strict-assertion.js';
14
- import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
15
- import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength, } from './generate-validation-expression.js';
16
- /**
17
- * Generates the parser function name from a type name.
18
- * Converts "UserObject" to "parseUserObject".
19
- */
1
+ import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
2
+ import { quoteJsString } from "@amritk/helpers/quote-js-string";
3
+ import { refToName } from "@amritk/helpers/ref-to-name";
4
+ import { resolveRef } from "@amritk/helpers/resolve-ref";
5
+ import { safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasRequired, hasType, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
+ import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
8
+ import { findDiscriminator } from "../helpers/find-discriminator.js";
9
+ import { getDefaultValue } from "../helpers/get-default-value.js";
10
+ import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
11
+ import { assertNoUnsupportedKeywords } from "./assert-supported-keywords.js";
12
+ import { generateEnumCaseInsensitiveCoercion } from "./generate-enum-check.js";
13
+ import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion } from "./generate-strict-assertion.js";
14
+ import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName } from "./generate-type-checks.js";
15
+ import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength } from "./generate-validation-expression.js";
20
16
  const generateParserName = (typeName) => {
21
- return `parse${typeName}`;
17
+ return `parse${typeName}`;
22
18
  };
23
- /**
24
- * Object-literal key form that is safe for the runtime-dangerous name
25
- * `__proto__`. In an object literal `{ __proto__: v }` / `{ "__proto__": v }` is
26
- * the special prototype-setter syntax — it never creates an own property — so a
27
- * schema property literally named `__proto__` would silently vanish (and, for an
28
- * object value, reassign the result's prototype). The computed form
29
- * `{ ["__proto__"]: v }` creates a normal own property. Every other key keeps
30
- * the plain `safeKey` output, so the common path is unchanged.
31
- */
32
- const safeLiteralKey = (key) => (key === '__proto__' ? '["__proto__"]' : safeKey(key));
33
- /**
34
- * Checks if a property is required based on the schema's required array.
35
- */
19
+ const safeLiteralKey = (key) => key === "__proto__" ? '["__proto__"]' : safeKey(key);
36
20
  const isPropertyRequired = (key, schema) => {
37
- if (!isSchemaObject(schema)) {
38
- return false;
39
- }
40
- if (!('required' in schema) || !Array.isArray(schema.required)) {
41
- return false;
42
- }
43
- return schema.required.includes(key);
21
+ if (!isSchemaObject(schema)) {
22
+ return false;
23
+ }
24
+ if (!("required" in schema) || !Array.isArray(schema.required)) {
25
+ return false;
26
+ }
27
+ return schema.required.includes(key);
44
28
  };
45
- /**
46
- * Generates a parser call expression for a required $ref property.
47
- * Example: parseContact(input.contact)
48
- */
49
29
  const generateRequiredRefCall = (key, ref, suffix) => {
50
- const acc = safeAccessor('input', key);
51
- const parserName = generateParserName(refToName(ref, suffix));
52
- return `${parserName}(${acc})`;
30
+ const acc = safeAccessor("input", key);
31
+ const parserName = generateParserName(refToName(ref, suffix));
32
+ return `${parserName}(${acc})`;
53
33
  };
54
- /**
55
- * Generates a parser call expression for an optional $ref property.
56
- * Example: ...(input.contact && { contact: parseContact(input.contact) })
57
- */
58
34
  const generateOptionalRefCall = (key, ref, suffix) => {
59
- const acc = safeAccessor('input', key);
60
- const parserName = generateParserName(refToName(ref, suffix));
61
- // Gate on presence, not truthiness: a `&&` guard skips parsing when the value
62
- // is `false`/`0`/`""`/`null`, spreading it raw. `!== undefined` still omits an
63
- // absent optional property but coerces every present one, matching the main path.
64
- return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${acc}) })`;
35
+ const acc = safeAccessor("input", key);
36
+ const parserName = generateParserName(refToName(ref, suffix));
37
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${acc}) })`;
65
38
  };
66
- /**
67
- * Generates a validateArray call for required array properties with $ref items.
68
- * Example: validateArray(input.contacts, parseContact)
69
- */
70
39
  const generateRequiredArrayRefCall = (key, ref, suffix) => {
71
- const parserName = generateParserName(refToName(ref, suffix));
72
- return `validateArray(${safeAccessor('input', key)}, ${parserName})`;
40
+ const parserName = generateParserName(refToName(ref, suffix));
41
+ return `validateArray(${safeAccessor("input", key)}, ${parserName})`;
73
42
  };
74
- /**
75
- * Generates a validateArray call for optional array properties with $ref items.
76
- * Example: ...(input.contacts && { contacts: validateArray(input.contacts, parseContact) })
77
- */
78
43
  const generateOptionalArrayRefCall = (key, ref, suffix) => {
79
- const parserName = generateParserName(refToName(ref, suffix));
80
- const acc = safeAccessor('input', key);
81
- // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
82
- return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateArray(${acc}, ${parserName}) })`;
44
+ const parserName = generateParserName(refToName(ref, suffix));
45
+ const acc = safeAccessor("input", key);
46
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateArray(${acc}, ${parserName}) })`;
83
47
  };
84
- /**
85
- * Generates a validateRecord call for required object properties with additionalProperties $ref.
86
- * Example: validateRecord(input.responses, parseResponse)
87
- */
88
48
  const generateRequiredRecordRefCall = (key, ref, suffix) => {
89
- const acc = safeAccessor('input', key);
90
- const parserName = generateParserName(refToName(ref, suffix));
91
- return `validateRecord(${acc}, ${parserName})`;
49
+ const acc = safeAccessor("input", key);
50
+ const parserName = generateParserName(refToName(ref, suffix));
51
+ return `validateRecord(${acc}, ${parserName})`;
92
52
  };
93
- /**
94
- * Generates a validateRecord call for optional object properties with additionalProperties $ref.
95
- * Example: ...(input.responses && { responses: validateRecord(input.responses, parseResponse) })
96
- */
97
53
  const generateOptionalRecordRefCall = (key, ref, suffix) => {
98
- const acc = safeAccessor('input', key);
99
- const parserName = generateParserName(refToName(ref, suffix));
100
- // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
101
- return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${acc}, ${parserName}) })`;
54
+ const acc = safeAccessor("input", key);
55
+ const parserName = generateParserName(refToName(ref, suffix));
56
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${acc}, ${parserName}) })`;
102
57
  };
103
- /**
104
- * Generates a spread entry for optional inline properties.
105
- * The value expression is evaluated once and omitted when undefined.
106
- */
107
58
  const generateOptionalInlineProperty = (key, valueExpression) => {
108
- return `...((value => value === undefined ? {} : { ${safeLiteralKey(key)}: value })(${valueExpression}))`;
59
+ return `...((value => value === undefined ? {} : { ${safeLiteralKey(key)}: value })(${valueExpression}))`;
109
60
  };
110
- /**
111
- * Determines if a property schema should use ref imports for validation.
112
- */
113
61
  const shouldUseRefImport = (propSchema, useRefImports) => {
114
- if (!useRefImports || !hasRef(propSchema)) {
115
- return false;
116
- }
117
- const ref = propSchema.$ref;
118
- // Skip external URI refs with property/definition fragments — these are not generated as files
119
- const isUri = ref.startsWith('http://') || ref.startsWith('https://');
120
- if (isUri && (ref.includes('#/properties/') || ref.includes('#/definitions/'))) {
121
- return false;
122
- }
123
- return true;
62
+ if (!useRefImports || !hasRef(propSchema)) {
63
+ return false;
64
+ }
65
+ const ref = propSchema.$ref;
66
+ const isUri = ref.startsWith("http://") || ref.startsWith("https://");
67
+ if (isUri && (ref.includes("#/properties/") || ref.includes("#/definitions/"))) {
68
+ return false;
69
+ }
70
+ return true;
124
71
  };
125
- /**
126
- * Determines if an array property should use ref imports for its items.
127
- */
128
72
  const shouldUseArrayRefImport = (propSchema, useRefImports) => {
129
- if (!useRefImports) {
130
- return false;
131
- }
132
- if (!isSchemaObject(propSchema)) {
133
- return false;
134
- }
135
- if (!('type' in propSchema) || propSchema.type !== 'array') {
136
- return false;
137
- }
138
- return hasItems(propSchema) && hasRef(propSchema.items);
73
+ if (!useRefImports) {
74
+ return false;
75
+ }
76
+ if (!isSchemaObject(propSchema)) {
77
+ return false;
78
+ }
79
+ if (!("type" in propSchema) || propSchema.type !== "array") {
80
+ return false;
81
+ }
82
+ return hasItems(propSchema) && hasRef(propSchema.items);
139
83
  };
140
- /**
141
- * Determines if a property with additionalProperties should use ref imports.
142
- */
143
84
  const shouldUseRecordRefImport = (propSchema, useRefImports) => {
144
- if (!useRefImports) {
145
- return false;
146
- }
147
- if (!isSchemaObject(propSchema)) {
148
- return false;
149
- }
150
- if (!('type' in propSchema) || propSchema.type !== 'object') {
151
- return false;
152
- }
153
- if (!('additionalProperties' in propSchema)) {
154
- return false;
155
- }
156
- const additionalProps = propSchema.additionalProperties;
157
- return isSchemaObject(additionalProps) && hasRef(additionalProps);
85
+ if (!useRefImports) {
86
+ return false;
87
+ }
88
+ if (!isSchemaObject(propSchema)) {
89
+ return false;
90
+ }
91
+ if (!("type" in propSchema) || propSchema.type !== "object") {
92
+ return false;
93
+ }
94
+ if (!("additionalProperties" in propSchema)) {
95
+ return false;
96
+ }
97
+ const additionalProps = propSchema.additionalProperties;
98
+ return isSchemaObject(additionalProps) && hasRef(additionalProps);
158
99
  };
159
- /**
160
- * Generates the value expression for a property based on its schema and options.
161
- */
162
100
  const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix, caseInsensitive = false) => {
163
- // Handle direct $ref properties
164
- if (shouldUseRefImport(propSchema, useRefImports)) {
165
- const ref = propSchema.$ref;
166
- return isRequired ? generateRequiredRefCall(key, ref, suffix) : generateOptionalRefCall(key, ref, suffix);
167
- }
168
- // Handle array properties with $ref items
169
- if (shouldUseArrayRefImport(propSchema, useRefImports)) {
170
- const items = propSchema.items;
171
- const ref = items.$ref;
172
- return isRequired ? generateRequiredArrayRefCall(key, ref, suffix) : generateOptionalArrayRefCall(key, ref, suffix);
173
- }
174
- // Handle object properties with additionalProperties $ref
175
- if (shouldUseRecordRefImport(propSchema, useRefImports)) {
176
- const additionalProps = propSchema.additionalProperties;
177
- const ref = additionalProps.$ref;
178
- return isRequired
179
- ? generateRequiredRecordRefCall(key, ref, suffix)
180
- : generateOptionalRecordRefCall(key, ref, suffix);
181
- }
182
- // Handle non-schema object properties (true/false)
183
- if (!isSchemaObject(propSchema)) {
184
- return isRequired ? 'undefined' : generateOptionalInlineProperty(key, 'undefined');
185
- }
186
- // Generate standard validation expression
187
- const defaultValue = getDefaultValue(propSchema);
188
- const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired, undefined, undefined, undefined, undefined, caseInsensitive);
189
- return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression);
101
+ if (shouldUseRefImport(propSchema, useRefImports)) {
102
+ const ref = propSchema.$ref;
103
+ return isRequired ? generateRequiredRefCall(key, ref, suffix) : generateOptionalRefCall(key, ref, suffix);
104
+ }
105
+ if (shouldUseArrayRefImport(propSchema, useRefImports)) {
106
+ const items = propSchema.items;
107
+ const ref = items.$ref;
108
+ return isRequired ? generateRequiredArrayRefCall(key, ref, suffix) : generateOptionalArrayRefCall(key, ref, suffix);
109
+ }
110
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
111
+ const additionalProps = propSchema.additionalProperties;
112
+ const ref = additionalProps.$ref;
113
+ return isRequired ? generateRequiredRecordRefCall(key, ref, suffix) : generateOptionalRecordRefCall(key, ref, suffix);
114
+ }
115
+ if (!isSchemaObject(propSchema)) {
116
+ return isRequired ? "undefined" : generateOptionalInlineProperty(key, "undefined");
117
+ }
118
+ const defaultValue = getDefaultValue(propSchema);
119
+ const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired, void 0, void 0, void 0, void 0, caseInsensitive);
120
+ return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression);
190
121
  };
191
- /**
192
- * Generates property entries for all properties in the schema.
193
- */
194
122
  const generatePropertyEntries = (schema, useRefImports, suffix, caseInsensitive = false) => {
195
- if (!hasProperties(schema)) {
196
- return [];
197
- }
198
- const entries = [];
199
- for (const [key, propSchema] of Object.entries(schema.properties)) {
200
- const isRequired = isPropertyRequired(key, schema);
201
- const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix, caseInsensitive);
202
- // Determine if the property uses spread syntax (making it optional in the object literal)
203
- const isOptional = value.startsWith('...');
204
- entries.push({ key, value, isOptional });
205
- }
206
- return entries;
123
+ if (!hasProperties(schema)) {
124
+ return [];
125
+ }
126
+ const entries = [];
127
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
128
+ const isRequired = isPropertyRequired(key, schema);
129
+ const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix, caseInsensitive);
130
+ const isOptional = value.startsWith("...");
131
+ entries.push({ key, value, isOptional });
132
+ }
133
+ return entries;
207
134
  };
208
- /**
209
- * Generates a fallback value for a required property.
210
- * This returns simple type-based defaults without pattern inference,
211
- * matching TypeBox's coercion behavior.
212
- */
213
135
  const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
214
- // Handle direct $ref properties - call the parser with undefined
215
- if (useRefImports && hasRef(propSchema)) {
216
- const ref = propSchema.$ref;
217
- const parserName = generateParserName(refToName(ref, suffix));
218
- return `${parserName}(undefined)`;
219
- }
220
- // Handle array properties with $ref items
221
- if (useRefImports &&
222
- isSchemaObject(propSchema) &&
223
- propSchema.type === 'array' &&
224
- hasItems(propSchema) &&
225
- hasRef(propSchema.items)) {
226
- return '[]';
227
- }
228
- // Handle object properties with additionalProperties $ref
229
- if (useRefImports &&
230
- isSchemaObject(propSchema) &&
231
- propSchema.type === 'object' &&
232
- 'additionalProperties' in propSchema) {
233
- const additionalProps = propSchema.additionalProperties;
234
- if (isSchemaObject(additionalProps) && hasRef(additionalProps)) {
235
- return '{}';
236
- }
136
+ if (useRefImports && hasRef(propSchema)) {
137
+ const ref = propSchema.$ref;
138
+ const parserName = generateParserName(refToName(ref, suffix));
139
+ return `${parserName}(undefined)`;
140
+ }
141
+ if (useRefImports && isSchemaObject(propSchema) && propSchema.type === "array" && hasItems(propSchema) && hasRef(propSchema.items)) {
142
+ return "[]";
143
+ }
144
+ if (useRefImports && isSchemaObject(propSchema) && propSchema.type === "object" && "additionalProperties" in propSchema) {
145
+ const additionalProps = propSchema.additionalProperties;
146
+ if (isSchemaObject(additionalProps) && hasRef(additionalProps)) {
147
+ return "{}";
237
148
  }
238
- // Everything else (default/const/enum/examples/union, the null type, and a
239
- // recursively-defaulted object) shares one source of truth so the fallback
240
- // object is itself a valid instance — `getDefaultValue` handles them all.
241
- return getDefaultValue(propSchema);
149
+ }
150
+ return getDefaultValue(propSchema);
242
151
  };
243
- /**
244
- * Generates a fallback object with required properties filled with default values.
245
- * This is used when input is not an object (undefined, null, etc.).
246
- */
247
152
  const generateFallbackObject = (schema, useRefImports, typeName, suffix, subTypeNames) => {
248
- if (!hasProperties(schema)) {
249
- return `{} as ${typeName}`;
250
- }
251
- // For schemas with conditional branches (if/then/else), the merged properties
252
- // may not fully represent all required fields. Use a simple cast to avoid
253
- // generating incomplete fallback objects.
254
- if (isSchemaObject(schema) && ('if' in schema || 'then' in schema || 'else' in schema)) {
255
- return `{} as ${typeName}`;
256
- }
257
- const requiredProps = [];
258
- for (const [key, propSchema] of Object.entries(schema.properties)) {
259
- const isRequired = isPropertyRequired(key, schema);
260
- if (isRequired) {
261
- // Inline nested objects delegate to their sub-parser so the fallback
262
- // carries proper deep defaults instead of an empty object literal.
263
- const subName = subTypeNames?.get(key);
264
- const fallbackValue = subName
265
- ? `${generateParserName(subName)}(undefined)`
266
- : generateFallbackValue(key, propSchema, useRefImports, suffix);
267
- if (fallbackValue === 'undefined') {
268
- return `{} as ${typeName}`;
269
- }
270
- requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
271
- }
272
- }
273
- if (requiredProps.length === 0) {
153
+ if (!hasProperties(schema)) {
154
+ return `{} as ${typeName}`;
155
+ }
156
+ if (isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
157
+ return `{} as ${typeName}`;
158
+ }
159
+ const requiredProps = [];
160
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
161
+ const isRequired = isPropertyRequired(key, schema);
162
+ if (isRequired) {
163
+ const subName = subTypeNames?.get(key);
164
+ const fallbackValue = subName ? `${generateParserName(subName)}(undefined)` : generateFallbackValue(key, propSchema, useRefImports, suffix);
165
+ if (fallbackValue === "undefined") {
274
166
  return `{} as ${typeName}`;
167
+ }
168
+ requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
275
169
  }
276
- let result = '{\n';
277
- for (let i = 0; i < requiredProps.length; i++) {
278
- result += requiredProps[i];
279
- if (i < requiredProps.length - 1) {
280
- result += '\n';
281
- }
170
+ }
171
+ if (requiredProps.length === 0) {
172
+ return `{} as ${typeName}`;
173
+ }
174
+ let result = "{\n";
175
+ for (let i = 0; i < requiredProps.length; i++) {
176
+ result += requiredProps[i];
177
+ if (i < requiredProps.length - 1) {
178
+ result += "\n";
282
179
  }
283
- result += '\n }';
284
- return result;
180
+ }
181
+ result += "\n }";
182
+ return result;
285
183
  };
286
- /**
287
- * Generates a parser for non-object schemas (string, number, boolean, array, etc.)
288
- * that validates the input matches the expected primitive type before casting.
289
- */
290
- /**
291
- * A default value literal (as source text) for `schema`, used as the fallback a
292
- * top-level union coerces an unmatched value to. Prefers a `const`/`enum` member,
293
- * then a per-type empty value.
294
- */
295
184
  const scalarDefaultLiteral = (schema) => {
296
- if (!isSchemaObject(schema))
297
- return '{}';
298
- if (hasConst(schema))
299
- return JSON.stringify(schema.const);
300
- if (hasEnum(schema) && schema.enum.length > 0)
301
- return JSON.stringify(schema.enum[0]);
302
- if (hasType(schema)) {
303
- switch (schema.type) {
304
- case 'string':
305
- return '""';
306
- case 'number':
307
- case 'integer':
308
- return '0';
309
- case 'boolean':
310
- return 'false';
311
- case 'null':
312
- return 'null';
313
- case 'array':
314
- return '[]';
315
- case 'object':
316
- return '{}';
317
- }
185
+ if (!isSchemaObject(schema))
186
+ return "{}";
187
+ if (hasConst(schema))
188
+ return JSON.stringify(schema.const);
189
+ if (hasEnum(schema) && schema.enum.length > 0)
190
+ return JSON.stringify(schema.enum[0]);
191
+ if (hasType(schema)) {
192
+ switch (schema.type) {
193
+ case "string":
194
+ return '""';
195
+ case "number":
196
+ case "integer":
197
+ return "0";
198
+ case "boolean":
199
+ return "false";
200
+ case "null":
201
+ return "null";
202
+ case "array":
203
+ return "[]";
204
+ case "object":
205
+ return "{}";
318
206
  }
319
- return '{}';
207
+ }
208
+ return "{}";
320
209
  };
321
- /**
322
- * Emits a dispatcher for a top-level `oneOf`/`anyOf` whose branches are `$ref`s,
323
- * when the branches share a discriminator (a `const`/`enum` tag such as `kind`).
324
- * Each resolved branch's discriminant selects its imported parser, so a recursive
325
- * discriminated union (e.g. `Expr = Lit | BinOp`, where `BinOp` has `Expr`
326
- * children) is actually validated and dispatched instead of blindly cast. Returns
327
- * `null` when it can't be done (no root schema, no discriminator, a non-ref
328
- * branch, or ref imports disabled), so the caller keeps its passthrough fallback.
329
- */
330
210
  const generateRefUnionDispatch = (functionName, typeName, branches, strict, ctx) => {
331
- const rootSchema = ctx.rootSchema;
332
- if (!ctx.useRefImports || rootSchema === undefined)
333
- return null;
334
- if (branches.length === 0 || !branches.every((b) => isSchemaObject(b) && hasRef(b)))
335
- return null;
336
- // Resolve each branch ref so we can read its discriminant tag and derive its
337
- // imported parser name.
338
- const resolved = branches.map((b) => {
339
- const ref = b.$ref;
340
- return { parser: generateParserName(refToName(ref, ctx.suffix)), schema: resolveRef(ref, rootSchema) };
341
- });
342
- if (resolved.some((r) => !isSchemaObject(r.schema)))
343
- return null;
344
- const discriminator = findDiscriminator(resolved.map((r) => r.schema));
345
- if (discriminator === null)
346
- return null;
347
- const cases = [];
348
- for (const r of resolved) {
349
- const value = getDiscriminatorValue(r.schema, discriminator);
350
- if (value === null)
351
- return null;
352
- cases.push({ value, parser: r.parser });
353
- }
354
- // Read the discriminant safely: `null`/`undefined` is guarded (a property read
355
- // would throw), and a non-object primitive yields `undefined` (no branch matches).
356
- const access = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(discriminator)
357
- ? `.${discriminator}`
358
- : `[${JSON.stringify(discriminator)}]`;
359
- const discExpr = `input == null ? undefined : (input as Record<string, unknown>)${access}`;
360
- // Fold branches into a nested ternary in declaration order:
361
- // `_disc === "lit" ? parseLit(input) : _disc === "binop" ? parseBinop(input) : <fallback>`.
362
- // Unmatched: strict throws (not a member of the union); non-strict coerces via the
363
- // first branch's parser, preserving the coercing parser's lenient contract while
364
- // still running real validation/coercion instead of a blind cast.
365
- const fallback = strict
366
- ? `(() => { throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)}); })()`
367
- : `${cases[0]?.parser}(input)`;
368
- let expr = fallback;
369
- for (let i = cases.length - 1; i >= 0; i--) {
370
- const c = cases[i];
371
- if (c)
372
- expr = `_disc === ${JSON.stringify(c.value)} ? ${c.parser}(input) : ${expr}`;
373
- }
374
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n const _disc = ${discExpr};\n return ${expr};\n};`;
211
+ const rootSchema = ctx.rootSchema;
212
+ if (!ctx.useRefImports || rootSchema === void 0)
213
+ return null;
214
+ if (branches.length === 0 || !branches.every((b) => isSchemaObject(b) && hasRef(b)))
215
+ return null;
216
+ const resolved = branches.map((b) => {
217
+ const ref = b.$ref;
218
+ return { parser: generateParserName(refToName(ref, ctx.suffix)), schema: resolveRef(ref, rootSchema) };
219
+ });
220
+ if (resolved.some((r) => !isSchemaObject(r.schema)))
221
+ return null;
222
+ const discriminator = findDiscriminator(resolved.map((r) => r.schema));
223
+ if (discriminator === null)
224
+ return null;
225
+ const cases = [];
226
+ for (const r of resolved) {
227
+ const value = getDiscriminatorValue(r.schema, discriminator);
228
+ if (value === null)
229
+ return null;
230
+ cases.push({ value, parser: r.parser });
231
+ }
232
+ const access = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(discriminator) ? `.${discriminator}` : `[${JSON.stringify(discriminator)}]`;
233
+ const discExpr = `input == null ? undefined : (input as Record<string, unknown>)${access}`;
234
+ const fallback = strict ? `(() => { throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)}); })()` : `${cases[0]?.parser}(input)`;
235
+ let expr = fallback;
236
+ for (let i = cases.length - 1; i >= 0; i--) {
237
+ const c = cases[i];
238
+ if (c)
239
+ expr = `_disc === ${JSON.stringify(c.value)} ? ${c.parser}(input) : ${expr}`;
240
+ }
241
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
242
+ const _disc = ${discExpr};
243
+ return ${expr};
244
+ };`;
375
245
  };
376
246
  const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
377
- const functionName = generateParserName(typeName);
378
- // A ref-branch discriminated union dispatches to its branch parsers in *both*
379
- // modes (strict throws on an unmatched discriminant; non-strict coerces via the
380
- // first branch). This runs before the `!strict` coercion block below so a strict
381
- // recursive union is validated instead of blindly cast.
382
- if (unionCtx && isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema))) {
383
- const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
384
- if (branches.length > 0 && branches.every((b) => isSchemaObject(b) && hasRef(b))) {
385
- const dispatch = generateRefUnionDispatch(functionName, typeName, branches, strict, unionCtx);
386
- if (dispatch !== null)
387
- return dispatch;
388
- }
247
+ const functionName = generateParserName(typeName);
248
+ if (unionCtx && isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema))) {
249
+ const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
250
+ if (branches.length > 0 && branches.every((b) => isSchemaObject(b) && hasRef(b))) {
251
+ const dispatch = generateRefUnionDispatch(functionName, typeName, branches, strict, unionCtx);
252
+ if (dispatch !== null)
253
+ return dispatch;
389
254
  }
390
- // An alias definition (a bare `$ref` with no shape of its own, e.g. a root
391
- // schema that is just `$ref: '#/$defs/expr'`) delegates to the referenced
392
- // parser instead of blindly casting, in both modes. Guarded against
393
- // self-reference: delegating to ourselves would recurse forever.
394
- if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
395
- const refName = refToName(schema.$ref, unionCtx.suffix);
396
- if (refName !== typeName) {
397
- return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
398
- }
399
- }
400
- // Strict union enforcement for inline (or mixed) branches: membership is the
401
- // disjunction of per-branch checks, and a non-member throws. Only emitted
402
- // when every branch check is false-sound (see canEnforceUnion) — otherwise a
403
- // conservative stub validator somewhere in the graph could reject valid
404
- // input — and never when stripping unknown keys (see UnionParserContext).
405
- if (strict && unionCtx && isSchemaObject(schema)) {
406
- const branches = getUnionBranches(schema);
407
- if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
408
- const check = generateUnionCheck('input', branches, unionCtx.useRefImports, unionCtx.suffix);
409
- if (check !== null) {
410
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!(${check})) throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)});\n return input as ${typeName};\n};`;
411
- }
412
- }
413
- }
414
- // `const`/`enum` define the value space directly (the generated type is a
415
- // literal / literal-union), so a non-member must coerce to a valid member —
416
- // otherwise the returned value would not be of the declared type. This runs
417
- // before the `type` switch so e.g. `{ type: 'string', enum: [...] }` is covered,
418
- // and before the no-`type` bail so a bare `const`/`enum` schema is covered too.
419
- if (!strict && isSchemaObject(schema)) {
420
- if (hasConst(schema)) {
421
- const literal = JSON.stringify(schema.const);
422
- // The const is the only valid value, so the parser always yields it. For a
423
- // primitive we can keep the caller's value when it already equals the const
424
- // (`===` is a correct comparison); for an object/array, `===` would be a
425
- // (always-false) reference comparison, so just return the const literal.
426
- const isPrimitive = schema.const === null || typeof schema.const !== 'object';
427
- return isPrimitive
428
- ? `export const ${functionName} = (input: unknown): ${typeName} => input === ${literal} ? input as ${typeName} : ${literal} as ${typeName};`
429
- : `export const ${functionName} = (input: unknown): ${typeName} => ${literal} as ${typeName};`;
430
- }
431
- if (hasEnum(schema) && schema.enum.length > 0) {
432
- const values = JSON.stringify(schema.enum);
433
- const fallback = JSON.stringify(schema.enum[0]);
434
- // Case-insensitive normalization sits on the non-member branch only, so an
435
- // exact member still returns via the `includes` fast path untouched.
436
- const ci = unionCtx?.caseInsensitive ? generateEnumCaseInsensitiveCoercion('input', schema.enum, fallback) : null;
437
- const coerced = ci ? `(${ci})` : fallback;
438
- return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${coerced} as ${typeName};`;
439
- }
440
- // A top-level union must validate membership: an unmatched value is not of
441
- // the declared union type, so coerce it to a member-shaped default. Reuse the
442
- // same union validation the property path uses.
443
- if (hasOneOf(schema) || hasAnyOf(schema)) {
444
- const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
445
- const hasRefBranch = branches.some((b) => isSchemaObject(b) && hasRef(b));
446
- if (branches.length > 0 && !hasRefBranch) {
447
- const fallback = scalarDefaultLiteral(branches[0]);
448
- const expr = generateValidationExpression('', schema, fallback, true, undefined, undefined, 'input', true);
449
- return `export const ${functionName} = (input: unknown): ${typeName} => (${expr}) as ${typeName};`;
450
- }
451
- // Ref-branch union: a *discriminated* one was already dispatched above (both
452
- // modes). Anything reaching here is non-discriminated, which can't be validated
453
- // inline without risking dropping valid input — fall back to a passthrough cast.
454
- return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
455
- }
456
- }
457
- // Strict counterpart of the block above for a *type-less* `const`/`enum` root
458
- // (`{ enum: [...] }` / `{ const: ... }` with no `type`): the no-`type` bail
459
- // below would otherwise return a bare cast, so strict mode has to assert
460
- // membership here. A typed `const`/`enum` (`{ type: 'string', enum: [...] }`)
461
- // keeps flowing to the strict scalar path, which asserts it via
462
- // generateScalarStrictAssertion.
463
- if (strict && isSchemaObject(schema) && !hasType(schema) && (hasConst(schema) || hasEnum(schema))) {
464
- const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
465
- if (assertion !== null) {
466
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n${assertion}\n return input as ${typeName};\n};`;
467
- }
255
+ }
256
+ if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
257
+ const refName = refToName(schema.$ref, unionCtx.suffix);
258
+ if (refName !== typeName) {
259
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
468
260
  }
469
- // Root-level arrays with rich item schemas delegate every element to a real
470
- // parser, so nested enums and $refs inside array items are validated instead
471
- // of spread through unchecked. $ref items call the imported parser; inline
472
- // object items get a private item sub-parser in the same file. (Scalar and
473
- // enum items are covered below: the lax switch coerces them element-wise and
474
- // generateScalarStrictAssertion enforces them in strict mode.)
475
- if (isSchemaObject(schema) &&
476
- hasType(schema) &&
477
- schema.type === 'array' &&
478
- hasItems(schema) &&
479
- !Array.isArray(schema.items)) {
480
- const items = schema.items;
481
- const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
482
- // This delegating path bypasses generateScalarStrictAssertion, so enforce the
483
- // array-level `contains` constraint here too — otherwise a root array of
484
- // objects/$refs would silently ignore it in strict mode.
485
- const strictArrayChecks = strict ? generateContainsCheck('input', schema, `[${typeName}]`).join('\n') : '';
486
- // validateArray identity-returns the input array when every element parses
487
- // to itself; this parser is EXPORTED, and exported parsers never alias the
488
- // value the caller passed in (matching the scalar root-array path's
489
- // `[...input]` copy), so materialize a copy exactly when that happens —
490
- // element references are still shared, like every other fast path.
491
- const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
492
- const delegated = (itemParserName) => strict
493
- ? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}${strictArrayChecks ? `\n${strictArrayChecks}` : ''}\n${delegatedBody(itemParserName)}\n};`
494
- : `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
495
- if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
496
- return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
497
- }
498
- if (isInlineObjectProperty(items)) {
499
- const reserved = unionCtx?.reservedNames ?? NO_RESERVED_NAMES;
500
- // Dedup against imported identifiers: a root `List` whose items carry a
501
- // $ref to `#/$defs/listItem` imports parseListItem — a bare `ListItem`
502
- // here would shadow it (TS2440) and self-recurse instead of delegating.
503
- let itemName = `${typeName}_Item`;
504
- while (reserved.has(itemName))
505
- itemName = `${itemName}_`;
506
- const useRefImports = unionCtx?.useRefImports ?? false;
507
- const suffix = unionCtx?.suffix ?? '';
508
- const stripUnknown = unionCtx?.stripUnknown ?? false;
509
- const preamble = [
510
- `type ${itemName} = ${typeName}[number];`,
511
- generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved),
512
- generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false),
513
- ].join('\n\n');
514
- return `${preamble}\n\n${delegated(generateParserName(itemName))}`;
515
- }
261
+ }
262
+ if (strict && unionCtx && isSchemaObject(schema)) {
263
+ const branches = getUnionBranches(schema);
264
+ if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
265
+ const check = generateUnionCheck("input", branches, unionCtx.useRefImports, unionCtx.suffix);
266
+ if (check !== null) {
267
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
268
+ if (!(${check})) throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)});
269
+ return input as ${typeName};
270
+ };`;
271
+ }
516
272
  }
517
- if (!isSchemaObject(schema) || !hasType(schema)) {
518
- // Schema without type information cannot be validated beyond a cast
519
- return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
273
+ }
274
+ if (!strict && isSchemaObject(schema)) {
275
+ if (hasConst(schema)) {
276
+ const literal = JSON.stringify(schema.const);
277
+ const isPrimitive = schema.const === null || typeof schema.const !== "object";
278
+ return isPrimitive ? `export const ${functionName} = (input: unknown): ${typeName} => input === ${literal} ? input as ${typeName} : ${literal} as ${typeName};` : `export const ${functionName} = (input: unknown): ${typeName} => ${literal} as ${typeName};`;
279
+ }
280
+ if (hasEnum(schema) && schema.enum.length > 0) {
281
+ const values = JSON.stringify(schema.enum);
282
+ const fallback = JSON.stringify(schema.enum[0]);
283
+ const ci = unionCtx?.caseInsensitive ? generateEnumCaseInsensitiveCoercion("input", schema.enum, fallback) : null;
284
+ const coerced = ci ? `(${ci})` : fallback;
285
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${coerced} as ${typeName};`;
286
+ }
287
+ if (hasOneOf(schema) || hasAnyOf(schema)) {
288
+ const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
289
+ const hasRefBranch = branches.some((b) => isSchemaObject(b) && hasRef(b));
290
+ if (branches.length > 0 && !hasRefBranch) {
291
+ const fallback = scalarDefaultLiteral(branches[0]);
292
+ const expr = generateValidationExpression("", schema, fallback, true, void 0, void 0, "input", true);
293
+ return `export const ${functionName} = (input: unknown): ${typeName} => (${expr}) as ${typeName};`;
294
+ }
295
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
520
296
  }
521
- if (!strict && schema.type === 'null') {
522
- return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
297
+ }
298
+ if (strict && isSchemaObject(schema) && !hasType(schema) && (hasConst(schema) || hasEnum(schema))) {
299
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
300
+ if (assertion !== null) {
301
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
302
+ ${assertion}
303
+ return input as ${typeName};
304
+ };`;
523
305
  }
524
- if (strict) {
525
- const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
526
- if (assertion === null) {
527
- return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
528
- }
529
- const returnExpr = schema.type === 'array' ? `[...(input as readonly unknown[])] as ${typeName}` : `input as ${typeName}`;
530
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n${assertion}\n return ${returnExpr};\n};`;
306
+ }
307
+ if (isSchemaObject(schema) && hasType(schema) && schema.type === "array" && hasItems(schema) && !Array.isArray(schema.items)) {
308
+ const items = schema.items;
309
+ const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
310
+ const strictArrayChecks = strict ? generateContainsCheck("input", schema, `[${typeName}]`).join("\n") : "";
311
+ const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});
312
+ return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
313
+ const delegated = (itemParserName) => strict ? `export const ${functionName} = (input: unknown): ${typeName} => {
314
+ ${notArrayThrow}${strictArrayChecks ? `
315
+ ${strictArrayChecks}` : ""}
316
+ ${delegatedBody(itemParserName)}
317
+ };` : `export const ${functionName} = (input: unknown): ${typeName} => {
318
+ ${delegatedBody(itemParserName)}
319
+ };`;
320
+ if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
321
+ return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
322
+ }
323
+ if (isInlineObjectProperty(items)) {
324
+ const reserved = unionCtx?.reservedNames ?? NO_RESERVED_NAMES;
325
+ let itemName = `${typeName}_Item`;
326
+ while (reserved.has(itemName))
327
+ itemName = `${itemName}_`;
328
+ const useRefImports = unionCtx?.useRefImports ?? false;
329
+ const suffix = unionCtx?.suffix ?? "";
330
+ const stripUnknown = unionCtx?.stripUnknown ?? false;
331
+ const itemShapeValidator = generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved);
332
+ const preamble = [
333
+ `type ${itemName} = ${typeName}[number];`,
334
+ itemShapeValidator,
335
+ generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator)
336
+ ].join("\n\n");
337
+ return `${preamble}
338
+
339
+ ${delegated(generateParserName(itemName))}`;
531
340
  }
532
- switch (schema.type) {
533
- case 'string':
534
- return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`;
535
- case 'number':
536
- return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" ? input as ${typeName} : 0 as ${typeName};`;
537
- case 'integer':
538
- // `integer` rejects non-integral numbers; a bare typeof would accept `1.5`.
539
- return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" && Number.isInteger(input) ? input as ${typeName} : 0 as ${typeName};`;
540
- case 'boolean':
541
- return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
542
- case 'array': {
543
- // Tuple `prefixItems`: coerce each declared position and, under items:false,
544
- // drop elements past the tuple length. A non-array coerces to an empty array.
545
- const prefixItems = getPrefixItems(schema);
546
- if (prefixItems) {
547
- const mapped = generatePrefixItemsMap('input', prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, undefined, unionCtx?.caseInsensitive);
548
- return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
549
- }
550
- // Coerce each element when the item schema is a single scalar type or an enum.
551
- if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
552
- const item = schema.items;
553
- const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true, unionCtx?.caseInsensitive);
554
- return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? (input as unknown[]).map((_it) => ${itemExpr}) as ${typeName} : [] as ${typeName};`;
555
- }
556
- return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`;
557
- }
558
- default:
559
- return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
341
+ }
342
+ if (!isSchemaObject(schema) || !hasType(schema)) {
343
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
344
+ }
345
+ if (!strict && schema.type === "null") {
346
+ return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
347
+ }
348
+ if (strict) {
349
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
350
+ if (assertion === null) {
351
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
560
352
  }
353
+ const returnExpr = schema.type === "array" ? `[...(input as readonly unknown[])] as ${typeName}` : `input as ${typeName}`;
354
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
355
+ ${assertion}
356
+ return ${returnExpr};
357
+ };`;
358
+ }
359
+ switch (schema.type) {
360
+ case "string":
361
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`;
362
+ case "number":
363
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" ? input as ${typeName} : 0 as ${typeName};`;
364
+ case "integer":
365
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" && Number.isInteger(input) ? input as ${typeName} : 0 as ${typeName};`;
366
+ case "boolean":
367
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
368
+ case "array": {
369
+ const prefixItems = getPrefixItems(schema);
370
+ if (prefixItems) {
371
+ const mapped = generatePrefixItemsMap("input", prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, void 0, unionCtx?.caseInsensitive);
372
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
373
+ }
374
+ if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
375
+ const item = schema.items;
376
+ const itemExpr = generateValidationExpression("", item, getDefaultValue(item), true, void 0, void 0, "_it", true, unionCtx?.caseInsensitive);
377
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? (input as unknown[]).map((_it) => ${itemExpr}) as ${typeName} : [] as ${typeName};`;
378
+ }
379
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`;
380
+ }
381
+ default:
382
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
383
+ }
561
384
  };
562
- /**
563
- * Object-level keyword checks (`propertyNames` / `dependentSchemas` /
564
- * `dependentRequired`) rendered as a newline-prefixed prelude for the
565
- * specialized strict object parsers (empty-object, pattern-properties,
566
- * additionalProperties, combined) that build their result outside
567
- * generateObjectStrictAssertion. Each of those emits its own
568
- * `if (!isObject(input)) throw` first, so the checks run against a proven object
569
- * (`guard = false`). Returns `''` when the schema carries none of the keywords,
570
- * so existing output for every other schema is byte-for-byte unchanged.
571
- */
572
385
  const strictObjectKeywordPrelude = (schema, typeName) => {
573
- if (!isSchemaObject(schema))
574
- return '';
575
- const lines = generateObjectKeywordChecks('input', schema, `[${typeName}]`, false);
576
- return lines.length > 0 ? `\n${lines.join('\n')}` : '';
386
+ if (!isSchemaObject(schema))
387
+ return "";
388
+ const lines = generateObjectKeywordChecks("input", schema, `[${typeName}]`, false);
389
+ return lines.length > 0 ? `
390
+ ${lines.join("\n")}` : "";
577
391
  };
578
- /**
579
- * Generates a parser for empty object schemas or schemas with only additionalProperties.
580
- * Validates the input is an object before casting, falling back to an empty object.
581
- * Returns a shallow copy to avoid mutating the original input. `schema`, when
582
- * given, supplies any object-level keyword checks the strict parser must enforce.
583
- */
584
392
  const generateEmptyObjectParser = (typeName, strict, schema) => {
585
- const functionName = generateParserName(typeName);
586
- if (strict) {
587
- const prelude = schema !== undefined ? strictObjectKeywordPrelude(schema, typeName) : '';
588
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${prelude}\n return { ...input } as ${typeName};\n};`;
589
- }
590
- return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
393
+ const functionName = generateParserName(typeName);
394
+ if (strict) {
395
+ const prelude = schema !== void 0 ? strictObjectKeywordPrelude(schema, typeName) : "";
396
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
397
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${prelude}
398
+ return { ...input } as ${typeName};
399
+ };`;
400
+ }
401
+ return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
591
402
  };
592
- /**
593
- * Converts a property key to a safe local variable name for caching.
594
- * Replaces non-identifier characters with underscores.
595
- */
596
403
  const toVarName = (key) => {
597
- const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_');
598
- return `_${safe}`;
404
+ const safe = key.replace(/[^a-zA-Z0-9_$]/g, "_");
405
+ return `_${safe}`;
599
406
  };
600
- /** PascalCases a property key for use in a synthesized sub-type name. */
601
- const pascalCaseKey = (key) => key
602
- .replace(/[^a-zA-Z0-9_$]/g, '_')
603
- .split('_')
604
- .filter((part) => part.length > 0)
605
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
606
- .join('');
607
- /** Shared empty reserved-name set for callers with no import context. */
608
- const NO_RESERVED_NAMES = new Set();
609
- /**
610
- * Shared empty result for the common no-inline-sub-types case, so the per-node
611
- * calls (parser and validator, for every object node, on every generation)
612
- * allocate nothing. Callers only read from the maps.
613
- */
614
- const EMPTY_SUB_TYPES = { objects: new Map(), arrayItems: new Map() };
407
+ const pascalCaseKey = (key) => key.replace(/[^a-zA-Z0-9_$]/g, "_").split("_").filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
408
+ const NO_RESERVED_NAMES = /* @__PURE__ */ new Set();
409
+ const EMPTY_SUB_TYPES = { objects: /* @__PURE__ */ new Map(), arrayItems: /* @__PURE__ */ new Map() };
615
410
  const collectInlineSubTypes = (schema, typeName, reservedNames = NO_RESERVED_NAMES) => {
616
- if (!hasProperties(schema))
617
- return EMPTY_SUB_TYPES;
618
- const props = schema.properties;
619
- // Single pass with lazy allocation: most object nodes have neither inline
620
- // object properties nor inline-object array items, and this runs for every
621
- // node in both the parser and the validator — so nothing is allocated until
622
- // the first match, and the classification predicates run exactly once per
623
- // property (an eager pre-scan would re-evaluate them for matching schemas).
624
- let objects = null;
625
- let arrayItems = null;
626
- let used = null;
627
- const claim = (base) => {
628
- used ??= new Set();
629
- let subName = base;
630
- while (used.has(subName) || reservedNames.has(subName))
631
- subName = `${subName}_`;
632
- used.add(subName);
633
- return subName;
634
- };
635
- for (const key in props) {
636
- const propSchema = props[key];
637
- if (isInlineObjectProperty(propSchema)) {
638
- objects ??= new Map();
639
- objects.set(key, claim(`${typeName}_${pascalCaseKey(key) || 'Value'}`));
640
- }
641
- else if (isInlineObjectArrayProperty(propSchema)) {
642
- arrayItems ??= new Map();
643
- arrayItems.set(key, claim(`${typeName}_${pascalCaseKey(key) || 'Value'}Item`));
644
- }
411
+ if (!hasProperties(schema))
412
+ return EMPTY_SUB_TYPES;
413
+ const props = schema.properties;
414
+ let objects = null;
415
+ let arrayItems = null;
416
+ let used = null;
417
+ const claim = (base) => {
418
+ used ??= /* @__PURE__ */ new Set();
419
+ let subName = base;
420
+ while (used.has(subName) || reservedNames.has(subName))
421
+ subName = `${subName}_`;
422
+ used.add(subName);
423
+ return subName;
424
+ };
425
+ for (const key in props) {
426
+ const propSchema = props[key];
427
+ if (isInlineObjectProperty(propSchema)) {
428
+ objects ??= /* @__PURE__ */ new Map();
429
+ objects.set(key, claim(`${typeName}_${pascalCaseKey(key) || "Value"}`));
430
+ } else if (isInlineObjectArrayProperty(propSchema)) {
431
+ arrayItems ??= /* @__PURE__ */ new Map();
432
+ arrayItems.set(key, claim(`${typeName}_${pascalCaseKey(key) || "Value"}Item`));
645
433
  }
646
- if (objects === null && arrayItems === null)
647
- return EMPTY_SUB_TYPES;
648
- return { objects: objects ?? EMPTY_SUB_TYPES.objects, arrayItems: arrayItems ?? EMPTY_SUB_TYPES.arrayItems };
434
+ }
435
+ if (objects === null && arrayItems === null)
436
+ return EMPTY_SUB_TYPES;
437
+ return { objects: objects ?? EMPTY_SUB_TYPES.objects, arrayItems: arrayItems ?? EMPTY_SUB_TYPES.arrayItems };
649
438
  };
650
- /**
651
- * True when the parser must reject (strict mode) or strip (coerce mode) every
652
- * undeclared key: the schema sets `additionalProperties: false` and nothing
653
- * else (key patterns, composition) can legitimately introduce extra keys.
654
- */
655
439
  const hasStrictKeys = (schema) => {
656
- if (!isSchemaObject(schema) || !hasProperties(schema))
657
- return false;
658
- if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
659
- return false;
660
- if ('patternProperties' in schema)
661
- return false;
662
- if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema))
663
- return false;
664
- return true;
440
+ if (!isSchemaObject(schema) || !hasProperties(schema))
441
+ return false;
442
+ if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
443
+ return false;
444
+ if ("patternProperties" in schema)
445
+ return false;
446
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema))
447
+ return false;
448
+ return true;
665
449
  };
666
- /**
667
- * When every declared property is required (and every required key is
668
- * declared, with a real schema), the fast-path known-keys test can be an
669
- * own-key *count* comparison instead of a per-key `for..in` walk: the typed
670
- * property checks already prove all N declared keys are present, so
671
- * `Object.keys(input).length === N` proves there are no undeclared extras —
672
- * measurably cheaper on the hot path. Returns the declared-key count, or null
673
- * when the cheaper form would be unsound (an optional or non-schema property
674
- * breaks the presence proof).
675
- */
676
450
  const exactKeyCountOf = (schema) => {
677
- if (!isSchemaObject(schema) || !hasProperties(schema) || !hasRequired(schema))
678
- return null;
679
- const props = schema.properties;
680
- const required = schema.required;
681
- // Allocation-free for the common small object: this runs for every object
682
- // node, in both the parser and the validator, on every generation, and the
683
- // O(n²) `includes` beats building a Set for a handful of keys. Wide schemas
684
- // (generated API models) flip to a one-time Set so a 200-property object
685
- // doesn't pay ~20k string comparisons per node.
686
- const requiredLookup = required.length > 16 ? new Set(required) : null;
687
- let declaredCount = 0;
688
- for (const key in props) {
689
- declaredCount++;
690
- // Every declared key must be required, and its check must prove presence —
691
- // all schema-object checks fail on undefined, but a true/false schema
692
- // literal emits no check at all.
693
- const isRequired = requiredLookup !== null ? requiredLookup.has(key) : required.includes(key);
694
- if (!isRequired || !isSchemaObject(props[key]))
695
- return null;
696
- }
697
- // Every declared key is required (above) and the counts match, so the
698
- // required list is exactly the declared keys (a duplicated required entry
699
- // would leave some declared key uncovered and fail the loop).
700
- if (declaredCount === 0 || required.length !== declaredCount)
701
- return null;
702
- return declaredCount;
451
+ if (!isSchemaObject(schema) || !hasProperties(schema) || !hasRequired(schema))
452
+ return null;
453
+ const props = schema.properties;
454
+ const required = schema.required;
455
+ const requiredLookup = required.length > 16 ? new Set(required) : null;
456
+ let declaredCount = 0;
457
+ for (const key in props) {
458
+ declaredCount++;
459
+ const isRequired = requiredLookup !== null ? requiredLookup.has(key) : required.includes(key);
460
+ if (!isRequired || !isSchemaObject(props[key]))
461
+ return null;
462
+ }
463
+ if (declaredCount === 0 || required.length !== declaredCount)
464
+ return null;
465
+ return declaredCount;
703
466
  };
704
- /**
705
- * Determines if a property needs a local variable or can be inlined.
706
- * Schema-object properties are always cached: the slow-path ternary chain
707
- * reads each value 3-4x (typeof check, valid branch, undefined check, coerce),
708
- * so a single hoisted load is strictly cheaper than the inlined optional chain.
709
- */
710
467
  const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
711
- // Non-schema-object properties (true/false JSON Schema literals) generate
712
- // `undefined` with no value access, so caching would be wasted.
713
- return isSchemaObject(propSchema);
468
+ return isSchemaObject(propSchema);
714
469
  };
715
- /**
716
- * Generates a parser for object schemas with properties.
717
- *
718
- * Uses an optimized function body with:
719
- * - Early return for non-object input
720
- * - Selective variable caching (only when needed)
721
- * - Fast path that returns input directly when all properties are already valid
722
- * - Slow path with optimized expressions
723
- */
724
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false) => {
725
- const functionName = generateParserName(typeName);
726
- const exportPrefix = exported ? 'export ' : '';
727
- if (!hasProperties(schema)) {
728
- if (strict) {
729
- return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName)}\n return input as ${typeName};\n};`;
730
- }
731
- return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
732
- }
733
- const schemaProps = schema.properties;
734
- // Keys once, values via lookup — Object.entries allocates a tuple per
735
- // property and this runs for every object node on every generation.
736
- const propertyKeys = Object.keys(schemaProps);
737
- // Inline nested object properties get a private sub-parser (plus shape
738
- // predicate and type alias) in the same file, so their fields are parsed for
739
- // real instead of only passing an isObject check. The sub-parser recurses
740
- // through this same generator, so nesting works to any depth. Array
741
- // properties with inline object items get the same treatment for their
742
- // element type, so nested enums and $refs *inside array items* are validated
743
- // too (each element runs through the item sub-parser via validateArray).
744
- const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
745
- const preamble = [];
746
- for (const [key, subName] of subTypeNames) {
747
- const propSchema = schemaProps[key];
748
- preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
749
- preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
750
- preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
751
- }
752
- for (const [key, subName] of subItemNames) {
753
- const propSchema = schemaProps[key];
754
- const itemSchema = propSchema.items;
755
- // NonNullable strips the `| undefined` an optional array property carries.
756
- preamble.push(`type ${subName} = NonNullable<${typeName}[${JSON.stringify(key)}]>[number];`);
757
- preamble.push(generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
758
- // Hand-rolled loop instead of Array.prototype.every on the guard path — the
759
- // callback protocol costs a few percent on element-heavy hot paths.
760
- preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {\n for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;\n return true;\n};`);
761
- preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
470
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
471
+ const functionName = generateParserName(typeName);
472
+ const exportPrefix = exported ? "export " : "";
473
+ if (!hasProperties(schema)) {
474
+ if (strict) {
475
+ return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {
476
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName)}
477
+ return input as ${typeName};
478
+ };`;
762
479
  }
763
- // additionalProperties: false — the per-key "is this undeclared" test inlines
764
- // `!==` comparisons for a short key list and hoists a Set only for a long one
765
- // (see unknownKeyCheck). The predicate form is shared by the fast path and the
766
- // shape validator; strict mode throws with the offending key.
767
- // `additionalProperties: false` strips *and rejects* extras; `stripUnknown`
768
- // only strips them. Both build the result from declared properties alone, so
769
- // they share the same machinery (`stripKeys`): drop the `...input` spread and
770
- // gate the `{ ...input }` fast path on the known-keys predicate. Only the real
771
- // `strictKeys` makes an extra a hard error (the throw block further down).
772
- const strictKeys = hasStrictKeys(schema);
773
- const stripKeys = strictKeys || stripUnknown;
774
- // When every declared property is required, the fast-path no-extras test is
775
- // the cheaper own-key count (see exactKeyCountOf) and the `_hasOnlyKnownKeys`
776
- // predicate is not emitted at all — the shape validator derives the same
777
- // decision from the schema, so the cross-function contract stays in sync.
778
- const exactKeyCount = stripKeys ? exactKeyCountOf(schema) : null;
779
- const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
780
- if (stripKeys && exactKeyCount === null) {
781
- for (const declaration of strictKeyCheck.declarations) {
782
- preamble.push(`${declaration};`);
783
- }
784
- preamble.push(`const _hasOnlyKnownKeys${typeName} = (input: Record<string, unknown>): boolean => {\n for (const _k in input) if (${strictKeyCheck.isUnknown('_k')}) return false;\n return true;\n};`);
480
+ return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
481
+ }
482
+ const schemaProps = schema.properties;
483
+ const propertyKeys = Object.keys(schemaProps);
484
+ const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
485
+ const preamble = [];
486
+ for (const [key, subName] of subTypeNames) {
487
+ const propSchema = schemaProps[key];
488
+ preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
489
+ const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
490
+ preamble.push(subShapeValidator);
491
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator));
492
+ }
493
+ for (const [key, subName] of subItemNames) {
494
+ const propSchema = schemaProps[key];
495
+ const itemSchema = propSchema.items;
496
+ preamble.push(`type ${subName} = NonNullable<${typeName}[${JSON.stringify(key)}]>[number];`);
497
+ const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
498
+ preamble.push(itemShapeValidator);
499
+ preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {
500
+ for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;
501
+ return true;
502
+ };`);
503
+ preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator));
504
+ }
505
+ const strictKeys = hasStrictKeys(schema);
506
+ const stripKeys = strictKeys || stripUnknown;
507
+ const exactKeyCount = stripKeys ? exactKeyCountOf(schema) : null;
508
+ const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
509
+ if (stripKeys && exactKeyCount === null) {
510
+ for (const declaration of strictKeyCheck.declarations) {
511
+ preamble.push(`${declaration};`);
512
+ }
513
+ preamble.push(`const _hasOnlyKnownKeys${typeName} = (input: Record<string, unknown>): boolean => {
514
+ for (const _k in input) if (${strictKeyCheck.isUnknown("_k")}) return false;
515
+ return true;
516
+ };`);
517
+ } else if (strict && strictKeys) {
518
+ for (const declaration of strictKeyCheck.declarations) {
519
+ preamble.push(`${declaration};`);
785
520
  }
786
- else if (strict && strictKeys) {
787
- // The unknown-key throw loop below still needs the hoisted Set (when the
788
- // key list is long enough to use one).
789
- for (const declaration of strictKeyCheck.declarations) {
790
- preamble.push(`${declaration};`);
791
- }
521
+ }
522
+ const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
523
+ const propInfo = [];
524
+ const hasAllOfRefParsers = useRefImports && isSchemaObject(schema) && hasAllOf(schema) && schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
525
+ let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
526
+ const fastPathChecks = [];
527
+ const fastPathAccessorChecks = [];
528
+ const usedVarNames = /* @__PURE__ */ new Set();
529
+ for (const key of propertyKeys) {
530
+ const propSchema = schemaProps[key];
531
+ const isRequired = isPropertyRequired(key, schema);
532
+ let varName = toVarName(key);
533
+ while (usedVarNames.has(varName))
534
+ varName = `${varName}_`;
535
+ usedVarNames.add(varName);
536
+ propInfo.push({ key, varName, isRequired, propSchema });
537
+ if (!canFastPath)
538
+ continue;
539
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
540
+ canFastPath = false;
541
+ continue;
542
+ }
543
+ const subName = subTypeNames.get(key);
544
+ let check = subName ? `${shapeValidatorName(subName)}(${varName})` : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
545
+ const itemSubName = subItemNames.get(key);
546
+ if (check !== null && itemSubName) {
547
+ check = `${check} && _every${itemSubName}(${varName})`;
548
+ }
549
+ if (check === null) {
550
+ canFastPath = false;
551
+ continue;
552
+ }
553
+ const accessor = safeAccessor("input", key);
554
+ let accessorCheck = subName ? `${shapeValidatorName(subName)}(${accessor})` : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
555
+ if (itemSubName) {
556
+ accessorCheck = `${accessorCheck} && _every${itemSubName}(${accessor})`;
557
+ }
558
+ if (isRequired) {
559
+ fastPathChecks.push(check);
560
+ fastPathAccessorChecks.push(accessorCheck);
561
+ } else {
562
+ fastPathChecks.push(`(${varName} === undefined || ${check})`);
563
+ fastPathAccessorChecks.push(`(${accessor} === undefined || ${accessorCheck})`);
792
564
  }
793
- const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
794
- // First pass: gather per-property info and decide whether a fast path is
795
- // possible. A fast path returns the input (or a direct strip-build) for
796
- // already-valid input without re-running per-property validation, so it may
797
- // only fire when every property can be cheaply proven in-shape.
798
- const propInfo = [];
799
- // Disable fast path when allOf $ref parsers need to be spread — the fast path
800
- // returns input as-is and would skip those coercions.
801
- const hasAllOfRefParsers = useRefImports &&
802
- isSchemaObject(schema) &&
803
- hasAllOf(schema) &&
804
- schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
805
- // Object-level keywords the fast path cannot mirror (dependentRequired,
806
- // dependentSchemas, propertyNames) force the slow path so the strict assertion
807
- // actually runs them; returning `{ ...input }` on a bare shape match would skip
808
- // the enforcement. (A property-level blocker like `contains` is already caught
809
- // per-property by generatePropertyTypeCheck returning null.)
810
- let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
811
- const fastPathChecks = [];
812
- // `toVarName` collapses distinct keys that differ only in non-identifier chars
813
- // (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
814
- // declarations (TS2451). Dedupe by suffixing `_` until unique — the same
815
- // approach `collectInlineObjectProperties` uses — so each key gets its own var.
816
- const usedVarNames = new Set();
817
- for (const key of propertyKeys) {
818
- const propSchema = schemaProps[key];
819
- const isRequired = isPropertyRequired(key, schema);
820
- let varName = toVarName(key);
821
- while (usedVarNames.has(varName))
822
- varName = `${varName}_`;
823
- usedVarNames.add(varName);
824
- propInfo.push({ key, varName, isRequired, propSchema });
825
- if (!canFastPath)
826
- continue;
827
- // Records of refs require iterating every value to check shape — too
828
- // expensive to inline into the parser's fast path. Disable.
829
- if (shouldUseRecordRefImport(propSchema, useRefImports)) {
830
- canFastPath = false;
831
- continue;
832
- }
833
- const subName = subTypeNames.get(key);
834
- // Inline nested objects fast-path through their private shape predicate, the
835
- // same way $ref properties use the imported one. Arrays of inline objects
836
- // additionally prove every element via the private item predicate.
837
- let check = subName
838
- ? `${shapeValidatorName(subName)}(${varName})`
839
- : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
840
- const itemSubName = subItemNames.get(key);
841
- if (check !== null && itemSubName) {
842
- check = `${check} && _every${itemSubName}(${varName})`;
843
- }
844
- if (check === null) {
845
- canFastPath = false;
846
- }
847
- else if (isRequired) {
848
- fastPathChecks.push(check);
849
- }
850
- else {
851
- fastPathChecks.push(`(${varName} === undefined || ${check})`);
852
- }
565
+ }
566
+ if (stripKeys) {
567
+ fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
568
+ }
569
+ const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
570
+ const literalReturnUsesVars = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema)) && exported;
571
+ let deepGuardExpr = deepGuard;
572
+ if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars) {
573
+ const shapeFnName = shapeValidatorName(typeName);
574
+ const shapeStrictKeysGuard = stripKeys && exactKeyCount === null ? `
575
+ if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
576
+ const shapeChecks = [...fastPathAccessorChecks];
577
+ if (stripKeys && exactKeyCount !== null) {
578
+ shapeChecks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}`);
579
+ }
580
+ const expectedShapeValidator = shapeChecks.length === 0 ? shapeStrictKeysGuard ? `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
581
+ if (!isObject(input)) return false;${shapeStrictKeysGuard}
582
+ return true;
583
+ };` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => isObject(input);` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
584
+ if (!isObject(input)) return false;${shapeStrictKeysGuard}
585
+ return ${shapeChecks.join("\n && ")};
586
+ };`;
587
+ if (shapeValidatorSource === expectedShapeValidator) {
588
+ deepGuardExpr = `${shapeFnName}(input)`;
853
589
  }
854
- // The `{ ...input }` fast path preserves extras, so when stripping it may only
855
- // fire on inputs that carry no undeclared key. For strict + additionalProperties:
856
- // false the cold path rejects extras instead; folding the known-keys term into
857
- // the guard lets the guard run *before* that rejection, so a valid clean input
858
- // never pays for the per-property assertions. With every declared property
859
- // required, the own-key count is equivalent and cheaper than the per-key walk —
860
- // but only for plain objects: a crafted prototype could satisfy the typed
861
- // checks through inherited properties while the own-key count still matches,
862
- // so non-plain inputs route to the slow path, where the for..in walk keeps
863
- // the historical inherited-key rejection.
864
- if (stripKeys) {
865
- fastPathChecks.push(exactKeyCount !== null
866
- ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}`
867
- : `_hasOnlyKnownKeys${typeName}(input)`);
590
+ }
591
+ const deepGuardCallsShape = deepGuardExpr !== deepGuard;
592
+ let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
593
+ const shallowChecks = [];
594
+ if (canShallowGuard) {
595
+ for (const { key, varName, isRequired, propSchema } of propInfo) {
596
+ const subName = subTypeNames.get(key);
597
+ let check;
598
+ if (subName) {
599
+ check = `isObject(${varName})`;
600
+ } else if (isSchemaObject(propSchema) && !hasEnum(propSchema) && !hasRef(propSchema) && !hasOneOf(propSchema) && !hasAnyOf(propSchema) && !hasAllOf(propSchema) && !("not" in propSchema) && !shouldUseRefImport(propSchema, useRefImports) && !shouldUseArrayRefImport(propSchema, useRefImports) && !shouldUseRecordRefImport(propSchema, useRefImports)) {
601
+ check = generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
602
+ } else {
603
+ check = null;
604
+ }
605
+ if (check === null) {
606
+ canShallowGuard = false;
607
+ break;
608
+ }
609
+ shallowChecks.push(isRequired ? check : `(${varName} === undefined || ${check})`);
868
610
  }
869
- // The deep guard proves the whole shape (so `{ ...input }` can be returned),
870
- // using the nested shape predicates and the known-keys term above.
871
- const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(' && ') : null;
872
- // The shallow guard powers the strict strip-build fast path (stripUnknown
873
- // without additionalProperties: false): it proves every scalar is well-typed
874
- // and every nested object is an object, but neither walks nested shapes deeply
875
- // nor requires the absence of extras. That lets it fire on the common
876
- // stripUnknown input — which carries extras the build removes, and nested
877
- // extras each sub-parser removes — while the cold path's per-property
878
- // assertions still produce the precise error on a genuine mismatch.
879
- let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
880
- const shallowChecks = [];
881
- if (canShallowGuard) {
882
- for (const { key, varName, isRequired, propSchema } of propInfo) {
883
- const subName = subTypeNames.get(key);
884
- let check;
885
- if (subName) {
886
- // Nested inline object: a shallow `isObject` is enough — the sub-parser
887
- // validates and strips it.
888
- check = `isObject(${varName})`;
889
- }
890
- else if (isSchemaObject(propSchema) &&
891
- !hasEnum(propSchema) &&
892
- !hasRef(propSchema) &&
893
- !hasOneOf(propSchema) &&
894
- !hasAnyOf(propSchema) &&
895
- !hasAllOf(propSchema) &&
896
- !('not' in propSchema) &&
897
- !shouldUseRefImport(propSchema, useRefImports) &&
898
- !shouldUseArrayRefImport(propSchema, useRefImports) &&
899
- !shouldUseRecordRefImport(propSchema, useRefImports)) {
900
- // Plain scalar / array / object: its own type check is already shallow
901
- // (arrays are not walked, objects only `isObject`-checked).
902
- check = generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
903
- }
904
- else {
905
- check = null;
906
- }
907
- if (check === null) {
908
- canShallowGuard = false;
909
- break;
910
- }
911
- shallowChecks.push(isRequired ? check : `(${varName} === undefined || ${check})`);
912
- }
611
+ }
612
+ const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(" && ") : null;
613
+ const varDeclLines = [];
614
+ for (const { key, varName, propSchema } of propInfo) {
615
+ if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
616
+ varDeclLines.push(` const ${varName} = ${safeAccessor("input", key)};`);
913
617
  }
914
- const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(' && ') : null;
915
- // Variable declarations for properties that need them (the build and guards
916
- // read each cached value once instead of re-accessing `input`).
917
- const varDeclLines = [];
918
- for (const { key, varName, propSchema } of propInfo) {
919
- if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
920
- varDeclLines.push(` const ${varName} = ${safeAccessor('input', key)};`);
618
+ }
619
+ const warnLines = [];
620
+ if (logWarnings && propInfo.length > 0) {
621
+ const warnKeyCheck = unknownKeyCheck(propInfo.map(({ key }) => key), "_knownKeys");
622
+ for (const declaration of warnKeyCheck.declarations) {
623
+ warnLines.push(` ${declaration};`);
624
+ }
625
+ warnLines.push(` for (const _k in input) {`);
626
+ warnLines.push(` if (${warnKeyCheck.isUnknown("_k")}) {`);
627
+ warnLines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`);
628
+ warnLines.push(` }`);
629
+ warnLines.push(` }`);
630
+ }
631
+ const buildObjectLines = (directAssign) => {
632
+ const objectLines = [];
633
+ if (!stripKeys) {
634
+ objectLines.push(" ...input,");
635
+ }
636
+ if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
637
+ for (const entry of schema.allOf) {
638
+ if (isSchemaObject(entry) && hasRef(entry)) {
639
+ const parserName = generateParserName(refToName(entry.$ref, suffix));
640
+ objectLines.push(` ...(${parserName}(input) as Record<string, unknown>),`);
921
641
  }
922
- }
923
- // Emit a warning for any input key not declared in the schema's properties.
924
- const warnLines = [];
925
- if (logWarnings && propInfo.length > 0) {
926
- const warnKeyCheck = unknownKeyCheck(propInfo.map(({ key }) => key), '_knownKeys');
927
- for (const declaration of warnKeyCheck.declarations) {
928
- warnLines.push(` ${declaration};`);
642
+ }
643
+ }
644
+ for (const { key, varName, isRequired, propSchema } of propInfo) {
645
+ const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
646
+ const accessor = shouldCache ? varName : safeAccessor("input", key);
647
+ const subName = subTypeNames.get(key);
648
+ if (subName) {
649
+ const subParserName = generateParserName(subName);
650
+ if (isRequired) {
651
+ objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
652
+ } else {
653
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
929
654
  }
930
- warnLines.push(` for (const _k in input) {`);
931
- warnLines.push(` if (${warnKeyCheck.isUnknown('_k')}) {`);
932
- warnLines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`);
933
- warnLines.push(` }`);
934
- warnLines.push(` }`);
935
- }
936
- // Builds the result object literal. `directAssign` (strict mode) assigns each
937
- // field straight from its cached read — the guard or the per-property
938
- // assertions have already proven the type, so no coercion ternary is needed.
939
- // Coerce mode keeps the type-checking-and-coercing expression. Refs, arrays of
940
- // refs, records of refs and inline nested objects always delegate to their
941
- // parser in both modes; the input spread is dropped whenever stripping.
942
- const buildObjectLines = (directAssign) => {
943
- const objectLines = [];
944
- if (!stripKeys) {
945
- objectLines.push(' ...input,');
655
+ continue;
656
+ }
657
+ const itemSubName = subItemNames.get(key);
658
+ if (itemSubName) {
659
+ const itemParserName = generateParserName(itemSubName);
660
+ if (isRequired) {
661
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
662
+ } else {
663
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
946
664
  }
947
- // Spread allOf $ref parsers so their field coercions are applied before
948
- // the explicit property validations below.
949
- if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
950
- for (const entry of schema.allOf) {
951
- if (isSchemaObject(entry) && hasRef(entry)) {
952
- const parserName = generateParserName(refToName(entry.$ref, suffix));
953
- objectLines.push(` ...(${parserName}(input) as Record<string, unknown>),`);
954
- }
955
- }
665
+ continue;
666
+ }
667
+ if (shouldUseRefImport(propSchema, useRefImports)) {
668
+ const ref = propSchema.$ref;
669
+ const parserName = generateParserName(refToName(ref, suffix));
670
+ if (isRequired) {
671
+ objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
672
+ } else {
673
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
956
674
  }
957
- for (const { key, varName, isRequired, propSchema } of propInfo) {
958
- const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
959
- const accessor = shouldCache ? varName : safeAccessor('input', key);
960
- // Handle inline nested objects via their private sub-parser, mirroring
961
- // how $ref properties delegate to the imported parser.
962
- const subName = subTypeNames.get(key);
963
- if (subName) {
964
- const subParserName = generateParserName(subName);
965
- if (isRequired) {
966
- objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
967
- }
968
- else {
969
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
970
- }
971
- continue;
972
- }
973
- // Arrays of inline objects run every element through the private item
974
- // sub-parser, the same way arrays of $refs delegate via validateArray.
975
- // In strict mode the item parser throws on a bad element; in coerce mode
976
- // it repairs the element to a valid instance.
977
- const itemSubName = subItemNames.get(key);
978
- if (itemSubName) {
979
- const itemParserName = generateParserName(itemSubName);
980
- if (isRequired) {
981
- objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
982
- }
983
- else {
984
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
985
- }
986
- continue;
987
- }
988
- // Handle direct $ref properties via imported parsers
989
- if (shouldUseRefImport(propSchema, useRefImports)) {
990
- const ref = propSchema.$ref;
991
- const parserName = generateParserName(refToName(ref, suffix));
992
- if (isRequired) {
993
- objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
994
- }
995
- else {
996
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
997
- }
998
- continue;
999
- }
1000
- // Handle array properties with $ref items
1001
- if (shouldUseArrayRefImport(propSchema, useRefImports)) {
1002
- const items = propSchema.items;
1003
- const ref = items.$ref;
1004
- const parserName = generateParserName(refToName(ref, suffix));
1005
- if (isRequired) {
1006
- objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
1007
- }
1008
- else {
1009
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
1010
- }
1011
- continue;
1012
- }
1013
- // Handle object properties with additionalProperties $ref
1014
- if (shouldUseRecordRefImport(propSchema, useRefImports)) {
1015
- const additionalProps = propSchema.additionalProperties;
1016
- const ref = additionalProps.$ref;
1017
- const parserName = generateParserName(refToName(ref, suffix));
1018
- if (isRequired) {
1019
- objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
1020
- }
1021
- else {
1022
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
1023
- }
1024
- continue;
1025
- }
1026
- // Handle non-schema-object properties
1027
- if (!isSchemaObject(propSchema)) {
1028
- if (isRequired) {
1029
- objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
1030
- }
1031
- continue;
1032
- }
1033
- // Strict mode has already proven the value's type (by guard or assertion),
1034
- // so the field is assigned straight from the cached read with no coercion.
1035
- let valueExpr;
1036
- if (directAssign) {
1037
- valueExpr = accessor;
1038
- }
1039
- else {
1040
- const defaultValue = getDefaultValue(propSchema);
1041
- // For optional properties we know the value is not undefined because
1042
- // we're inside the `...(accessor !== undefined && { ... })` check.
1043
- const knownNotUndefined = !isRequired;
1044
- valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
1045
- }
1046
- if (isRequired) {
1047
- objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
1048
- }
1049
- else {
1050
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
1051
- }
675
+ continue;
676
+ }
677
+ if (shouldUseArrayRefImport(propSchema, useRefImports)) {
678
+ const items = propSchema.items;
679
+ const ref = items.$ref;
680
+ const parserName = generateParserName(refToName(ref, suffix));
681
+ if (isRequired) {
682
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
683
+ } else {
684
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
1052
685
  }
1053
- return objectLines;
1054
- };
1055
- const emitReturn = (lines, objectLines) => {
1056
- lines.push(` return {`);
1057
- lines.push(objectLines.join('\n'));
1058
- lines.push(` } as unknown as ${typeName};`);
1059
- };
1060
- // The deep-guard fast path. `{ ...input }` is the only correct shape when the
1061
- // guard let undeclared keys through (no known-keys term) — they must survive.
1062
- // But when `stripKeys` is set the guard *also* proved `_hasOnlyKnownKeys`, so
1063
- // the input's keys are exactly the declared properties: an explicit literal of
1064
- // those keys is then equivalent to the spread, and faster — a fixed-shape
1065
- // literal beats a generic spread, yields a stable hidden class, and produces
1066
- // the same declared key order as the slow path (the spread used input order).
1067
- // The literal shares each value by reference, exactly like the spread did, so
1068
- // it never re-parses an already-validated nested object. allOf merges in
1069
- // properties this `propInfo` list doesn't carry, so it keeps the spread.
1070
- const emitDeepGuardReturn = (lines) => {
1071
- const canLiteral = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema));
1072
- if (!canLiteral) {
1073
- lines.push(` if (${deepGuard}) return { ...input } as ${typeName};`);
1074
- return;
686
+ continue;
687
+ }
688
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
689
+ const additionalProps = propSchema.additionalProperties;
690
+ const ref = additionalProps.$ref;
691
+ const parserName = generateParserName(refToName(ref, suffix));
692
+ if (isRequired) {
693
+ objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
694
+ } else {
695
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
1075
696
  }
1076
- // A *private* (nested-object / array-item) parser whose deep guard proved
1077
- // the input is exactly the declared shape — typed checks plus the
1078
- // no-undeclared-keys term, recursively via sub-predicates — can hand the
1079
- // input back by reference instead of allocating a literal. That is the
1080
- // same sharing the parent's own fast-path literal performs (`items:
1081
- // _items`), and it is what keeps clean array elements allocation-free.
1082
- // Exported root parsers keep returning a fresh object so callers never
1083
- // alias the value they passed in.
1084
- if (!exported) {
1085
- lines.push(` if (${deepGuard}) return input as ${typeName};`);
1086
- return;
697
+ continue;
698
+ }
699
+ if (!isSchemaObject(propSchema)) {
700
+ if (isRequired) {
701
+ objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
1087
702
  }
1088
- const fields = [];
1089
- for (const { key, varName, isRequired, propSchema } of propInfo) {
1090
- if (!isSchemaObject(propSchema)) {
1091
- if (isRequired)
1092
- fields.push(` ${safeLiteralKey(key)}: undefined,`);
1093
- continue;
1094
- }
1095
- const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports)
1096
- ? varName
1097
- : safeAccessor('input', key);
1098
- fields.push(isRequired
1099
- ? ` ${safeLiteralKey(key)}: ${accessor},`
1100
- : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
703
+ continue;
704
+ }
705
+ let valueExpr;
706
+ if (directAssign) {
707
+ valueExpr = accessor;
708
+ } else {
709
+ const defaultValue = getDefaultValue(propSchema);
710
+ const knownNotUndefined = !isRequired;
711
+ valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, void 0, void 0, shouldCache ? varName : void 0, knownNotUndefined, caseInsensitive);
712
+ }
713
+ if (isRequired) {
714
+ objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
715
+ } else {
716
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
717
+ }
718
+ }
719
+ return objectLines;
720
+ };
721
+ const emitReturn = (lines2, objectLines) => {
722
+ lines2.push(` return {`);
723
+ lines2.push(objectLines.join("\n"));
724
+ lines2.push(` } as unknown as ${typeName};`);
725
+ };
726
+ const emitDeepGuardReturn = (lines2) => {
727
+ const canLiteral = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema));
728
+ if (!canLiteral) {
729
+ lines2.push(` if (${deepGuardExpr}) return { ...input } as ${typeName};`);
730
+ return;
731
+ }
732
+ if (!exported) {
733
+ lines2.push(` if (${deepGuardExpr}) return input as ${typeName};`);
734
+ return;
735
+ }
736
+ const fields = [];
737
+ for (const { key, varName, isRequired, propSchema } of propInfo) {
738
+ if (!isSchemaObject(propSchema)) {
739
+ if (isRequired)
740
+ fields.push(` ${safeLiteralKey(key)}: undefined,`);
741
+ continue;
742
+ }
743
+ const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports) ? varName : safeAccessor("input", key);
744
+ fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
745
+ }
746
+ lines2.push(` if (${deepGuardExpr}) return {`);
747
+ lines2.push(fields.join("\n"));
748
+ lines2.push(` } as ${typeName};`);
749
+ };
750
+ const lines = [];
751
+ lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
752
+ const varsAfterGuard = deepGuardCallsShape && shallowGuard === null;
753
+ if (strict) {
754
+ lines.push(` if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`);
755
+ if (!varsAfterGuard)
756
+ lines.push(...varDeclLines);
757
+ lines.push(...warnLines);
758
+ const assertionLines = generateObjectStrictAssertion(schema, typeName, {
759
+ useRefImports,
760
+ suffix,
761
+ stripUnknown,
762
+ ...rootSchema !== void 0 ? { rootSchema } : {}
763
+ }).slice(1);
764
+ if (shallowGuard) {
765
+ if (!exported && deepGuard) {
766
+ const residual = [];
767
+ for (let i = 0; i < fastPathChecks.length; i++) {
768
+ const deep = fastPathChecks[i];
769
+ const shallow = shallowChecks[i];
770
+ if (deep === shallow)
771
+ continue;
772
+ residual.push(shallow !== void 0 && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
1101
773
  }
1102
- lines.push(` if (${deepGuard}) return {`);
1103
- lines.push(fields.join('\n'));
1104
- lines.push(` } as ${typeName};`);
1105
- };
1106
- const lines = [];
1107
- lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
1108
- if (strict) {
1109
- // Guard first: a non-object throws straight away; a clean, well-typed input
1110
- // then short-circuits past the per-property assertions, which only run to
1111
- // pinpoint the failure when the guard rejects the input.
1112
- lines.push(` if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`);
1113
- lines.push(...varDeclLines);
1114
- lines.push(...warnLines);
1115
- // The first assertion line is the `isObject` check, already done above.
1116
- const assertionLines = generateObjectStrictAssertion(schema, typeName, {
1117
- useRefImports,
1118
- suffix,
1119
- stripUnknown,
1120
- ...(rootSchema !== undefined ? { rootSchema } : {}),
1121
- }).slice(1);
1122
- if (shallowGuard) {
1123
- // A private sub-parser hands input that is already exactly the declared
1124
- // shape (no extras at any level) back by reference, so a clean array
1125
- // element or nested object costs no allocation. The deep guard is
1126
- // evaluated as the shallow guard plus only its *residual* terms (deeper
1127
- // per-property checks and the no-extras term), so a carries-extras input
1128
- // never runs the same typed checks twice before taking the strip build.
1129
- if (!exported && deepGuard) {
1130
- const residual = [];
1131
- for (let i = 0; i < fastPathChecks.length; i++) {
1132
- const deep = fastPathChecks[i];
1133
- const shallow = shallowChecks[i];
1134
- if (deep === shallow)
1135
- continue;
1136
- // When the deep check extends the shallow one by conjunction (e.g.
1137
- // `Array.isArray(_x) && _everyItem(_x)`), the shallow guard already
1138
- // proved the prefix — re-evaluating it per clean parse was pure
1139
- // waste on the hot path, so only the extension joins the residual.
1140
- residual.push(shallow !== undefined && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
1141
- }
1142
- lines.push(` if (${shallowGuard}) {`);
1143
- lines.push(residual.length > 0
1144
- ? ` if (${residual.join(' && ')}) return input as ${typeName};`
1145
- : ` return input as ${typeName};`);
1146
- lines.push(` } else {`);
1147
- for (const assertionLine of assertionLines) {
1148
- lines.push(` ${assertionLine}`);
1149
- }
1150
- lines.push(` }`);
1151
- }
1152
- else {
1153
- // stripUnknown: a well-typed input skips the assertions and goes
1154
- // straight to the strip build (which removes extras and recurses into
1155
- // sub-parsers).
1156
- lines.push(` if (!(${shallowGuard})) {`);
1157
- for (const assertionLine of assertionLines) {
1158
- lines.push(` ${assertionLine}`);
1159
- }
1160
- lines.push(` }`);
1161
- }
1162
- emitReturn(lines, buildObjectLines(true));
774
+ lines.push(` if (${shallowGuard}) {`);
775
+ lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) return input as ${typeName};` : ` return input as ${typeName};`);
776
+ lines.push(` } else {`);
777
+ for (const assertionLine of assertionLines) {
778
+ lines.push(` ${assertionLine}`);
1163
779
  }
1164
- else {
1165
- if (deepGuard) {
1166
- emitDeepGuardReturn(lines);
1167
- }
1168
- for (const assertionLine of assertionLines) {
1169
- lines.push(assertionLine);
1170
- }
1171
- if (strictKeys) {
1172
- // for..in (not Object.keys) deliberately: this loop is cold — the fast
1173
- // path already proved the key set — but swapping in the keys-array
1174
- // iterator here once regressed the *hot* path several percent on CI:
1175
- // the extra dead-path bytecode changed the engine's inlining of the
1176
- // whole parser. The fast-path no-extras test uses own-key semantics;
1177
- // an inherited-key mismatch merely lands here and keeps the historical
1178
- // for..in rejection.
1179
- lines.push(` for (const _k in input) {`);
1180
- lines.push(` if (${strictKeyCheck.isUnknown('_k')}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
1181
- lines.push(` }`);
1182
- }
1183
- emitReturn(lines, buildObjectLines(true));
780
+ lines.push(` }`);
781
+ } else {
782
+ lines.push(` if (!(${shallowGuard})) {`);
783
+ for (const assertionLine of assertionLines) {
784
+ lines.push(` ${assertionLine}`);
1184
785
  }
1185
- }
1186
- else {
1187
- lines.push(` if (!isObject(input)) return ${fallbackObject};`);
786
+ lines.push(` }`);
787
+ }
788
+ emitReturn(lines, buildObjectLines(true));
789
+ } else {
790
+ if (deepGuard) {
791
+ emitDeepGuardReturn(lines);
792
+ }
793
+ if (varsAfterGuard)
1188
794
  lines.push(...varDeclLines);
1189
- lines.push(...warnLines);
1190
- if (deepGuard) {
1191
- emitDeepGuardReturn(lines);
1192
- }
1193
- emitReturn(lines, buildObjectLines(false));
1194
- }
1195
- lines.push(`}`);
1196
- const fn = lines.join('\n');
1197
- if (preamble.length === 0)
1198
- return fn;
1199
- return `${preamble.join('\n\n')}\n\n${fn}`;
795
+ for (const assertionLine of assertionLines) {
796
+ lines.push(assertionLine);
797
+ }
798
+ if (strictKeys) {
799
+ lines.push(` for (const _k in input) {`);
800
+ lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
801
+ lines.push(` }`);
802
+ }
803
+ emitReturn(lines, buildObjectLines(true));
804
+ }
805
+ } else {
806
+ lines.push(` if (!isObject(input)) return ${fallbackObject};`);
807
+ if (!varsAfterGuard)
808
+ lines.push(...varDeclLines);
809
+ lines.push(...warnLines);
810
+ if (deepGuard) {
811
+ emitDeepGuardReturn(lines);
812
+ }
813
+ if (varsAfterGuard)
814
+ lines.push(...varDeclLines);
815
+ emitReturn(lines, buildObjectLines(false));
816
+ }
817
+ lines.push(`}`);
818
+ const fn = lines.join("\n");
819
+ if (preamble.length === 0)
820
+ return fn;
821
+ return `${preamble.join("\n\n")}
822
+
823
+ ${fn}`;
1200
824
  };
1201
- /**
1202
- * Combined-parser variant for `properties` + `patternProperties` schemas that
1203
- * set `additionalProperties: false`. Declared properties are coerced as usual,
1204
- * keys matching any pattern are kept (the first `$ref` pattern is coerced via
1205
- * its imported parser when ref imports are on), and every other key is rejected
1206
- * in strict mode or stripped in coerce mode — matching the interpreter.
1207
- */
1208
- /**
1209
- * Emits a prototype-safe `result[key] = value` for the dynamic `for..in` copy
1210
- * loops in the pattern-property parsers. A bare `result[key] = v` is a `[[Set]]`
1211
- * — and for the key `"__proto__"` (own-enumerable when the input came from
1212
- * `JSON.parse`) it invokes the inherited `Object.prototype` setter and reassigns
1213
- * `result`'s prototype to attacker-controlled data, the same vector
1214
- * `validateRecord` guards against. `"__proto__"` is the only own-enumerable key
1215
- * whose name resolves to an accessor on `Object.prototype` (`constructor` et al.
1216
- * are writable data properties, so a `[[Set]]` there just creates an own key),
1217
- * so a single-keyed guard is sufficient. Each loop already runs a `.test(key)`
1218
- * regex per iteration, which dominates the cost, so the extra `===` on the
1219
- * common path is immeasurable. Assumes the target variable is named `result`,
1220
- * as it is in every parser that calls this.
1221
- */
1222
825
  const safeResultAssign = (keyVar, valueExpr) => `if (${keyVar} === "__proto__") { Object.defineProperty(result, ${keyVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); } else { (result as Record<string, unknown>)[${keyVar}] = ${valueExpr}; }`;
1223
826
  const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
1224
- const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
1225
- // Below the inline threshold a key === "a" || key === "b" chain skips declared
1226
- // keys without the per-call Set allocation; a wider list hoists the Set.
1227
- const knownKeyCheck = unknownKeyCheck(declaredKeys, '_knownKeys');
1228
- // The first $ref pattern (when ref imports are on) coerces its matching values
1229
- // through the imported parser; the remaining patterns keep the raw value.
1230
- const refPattern = useRefImports ? patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps)) : undefined;
1231
- const loopLines = [` if (${knownKeyCheck.isKnown('key')}) continue;`];
1232
- if (refPattern) {
1233
- const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
1234
- loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
1235
- loopLines.push(` ${safeResultAssign('key', `${parserName}(input[key])`)}`);
1236
- loopLines.push(` continue;`);
1237
- loopLines.push(` }`);
1238
- }
1239
- const keepConditions = patterns
1240
- .filter(([p]) => !(refPattern && p === refPattern[0]))
1241
- .map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1242
- if (keepConditions.length > 0) {
1243
- loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1244
- loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1245
- loopLines.push(` continue;`);
1246
- loopLines.push(` }`);
1247
- }
1248
- // Unknown key: throw in strict mode, otherwise let it fall through (dropped,
1249
- // since it was never copied into `result`).
1250
- if (strict)
1251
- loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
1252
- const notObjectBranch = strict
1253
- ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1254
- : ` return {} as unknown as ${typeName};`;
1255
- const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
1256
- // Only the Set form needs a declaration; the inline === chain is stateless.
1257
- const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
1258
- // In strict mode assert the declared properties (type, required, enum,
1259
- // constraints) — the `result` below is built from the *coercing* property
1260
- // lines, which silently repair a wrong type and default a missing required key
1261
- // instead of throwing. `generateObjectStrictAssertion` throws first; the
1262
- // coercing build then runs only on already-valid input (a no-op there). It also
1263
- // covers the object-level keyword checks, so it replaces the keyword-only
1264
- // prelude. `.slice(1)` drops its `isObject` check (emitted just below). Coerce
1265
- // mode (stripUnknown) keeps its lighter prelude.
1266
- const assertionPrelude = strict
1267
- ? generateObjectStrictAssertion(schema, typeName, { useRefImports, suffix }).slice(1)
1268
- : [];
1269
- const keywordPrelude = assertionPrelude.length > 0 ? `\n${assertionPrelude.join('\n')}` : '';
1270
- return `export const ${functionName} = (input: unknown): ${typeName} => {
827
+ const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
828
+ const knownKeyCheck = unknownKeyCheck(declaredKeys, "_knownKeys");
829
+ const refPattern = useRefImports ? patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps)) : void 0;
830
+ const loopLines = [` if (${knownKeyCheck.isKnown("key")}) continue;`];
831
+ if (refPattern) {
832
+ const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
833
+ loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
834
+ loopLines.push(` ${safeResultAssign("key", `${parserName}(input[key])`)}`);
835
+ loopLines.push(` continue;`);
836
+ loopLines.push(` }`);
837
+ }
838
+ const keepConditions = patterns.filter(([p]) => !(refPattern && p === refPattern[0])).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
839
+ if (keepConditions.length > 0) {
840
+ loopLines.push(` if (${keepConditions.join(" || ")}) {`);
841
+ loopLines.push(` ${safeResultAssign("key", "input[key]")}`);
842
+ loopLines.push(` continue;`);
843
+ loopLines.push(` }`);
844
+ }
845
+ if (strict)
846
+ loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
847
+ const notObjectBranch = strict ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);` : ` return {} as unknown as ${typeName};`;
848
+ const resultBody = propertyLines.length > 0 ? `{
849
+ ${propertyLines.join("\n")}
850
+ }` : "{}";
851
+ const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};
852
+ `).join("");
853
+ const assertionPrelude = strict ? generateObjectStrictAssertion(schema, typeName, { useRefImports, suffix }).slice(1) : [];
854
+ const keywordPrelude = assertionPrelude.length > 0 ? `
855
+ ${assertionPrelude.join("\n")}` : "";
856
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1271
857
  if (!isObject(input)) {
1272
858
  ${notObjectBranch}
1273
859
  }${keywordPrelude}
1274
860
  ${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
1275
861
  for (const key in input) {
1276
- ${loopLines.join('\n')}
862
+ ${loopLines.join("\n")}
1277
863
  }
1278
864
  return result;
1279
865
  };`;
1280
866
  };
1281
- /**
1282
- * Generates a parser for schemas that have both properties AND patternProperties.
1283
- * Parses the known properties first, then iterates remaining keys to match patterns.
1284
- */
1285
867
  const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false) => {
1286
- const functionName = generateParserName(typeName);
1287
- const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
1288
- // Build the known property lines for the initial object
1289
- const propertyLines = entries.map((entry) => {
1290
- if (entry.isOptional) {
1291
- return ` ${entry.value},`;
1292
- }
1293
- return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
1294
- });
1295
- // Find the first pattern with a $ref for parser delegation
1296
- if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
1297
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1298
- }
1299
- const patternProps = schema.patternProperties;
1300
- const patterns = Object.entries(patternProps);
1301
- const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
1302
- const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1303
- // With `additionalProperties: false`, keys matching neither a declared
1304
- // property nor any pattern must be rejected (strict) or stripped (coerce).
1305
- // The blanket `...input` spread used below cannot do that, so build a
1306
- // selective copy that mirrors the interpreter's undeclared-key handling.
1307
- if (strictKeys) {
1308
- return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
1309
- }
1310
- // `stripUnknown` (coerce mode) must keep keys a `patternProperties` regex
1311
- // declares and drop only the genuinely-undeclared ones. Delegating to
1312
- // `generateObjectParser` below would strip the pattern-matching keys too — its
1313
- // strip logic only knows the declared `properties`. The selective combined
1314
- // copy keeps declared + pattern-matching keys, exactly like the interpreter.
1315
- if (stripUnknown && !strict) {
1316
- return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false);
1317
- }
1318
- if (!refPattern || !useRefImports) {
1319
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1320
- }
1321
- const [pattern, patternSchema] = refPattern;
1322
- const ref = patternSchema.$ref;
1323
- const parserName = generateParserName(refToName(ref, suffix));
1324
- const assignmentCode = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1325
- const escapedPattern = escapeRegexPattern(pattern);
1326
- const inputSpread = ' ...input,';
1327
- let objectProperties = inputSpread;
1328
- if (propertyLines.length > 0) {
1329
- for (const line of propertyLines) {
1330
- objectProperties += '\n' + line;
1331
- }
868
+ const functionName = generateParserName(typeName);
869
+ const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
870
+ const propertyLines = entries.map((entry) => {
871
+ if (entry.isOptional) {
872
+ return ` ${entry.value},`;
873
+ }
874
+ return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
875
+ });
876
+ if (!isSchemaObject(schema) || !("patternProperties" in schema)) {
877
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
878
+ }
879
+ const patternProps = schema.patternProperties;
880
+ const patterns = Object.entries(patternProps);
881
+ const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
882
+ const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
883
+ if (strictKeys) {
884
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
885
+ }
886
+ if (stripUnknown && !strict) {
887
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false);
888
+ }
889
+ if (!refPattern || !useRefImports) {
890
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
891
+ }
892
+ const [pattern, patternSchema] = refPattern;
893
+ const ref = patternSchema.$ref;
894
+ const parserName = generateParserName(refToName(ref, suffix));
895
+ const assignmentCode = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
896
+ const escapedPattern = escapeRegexPattern(pattern);
897
+ const inputSpread = " ...input,";
898
+ let objectProperties = inputSpread;
899
+ if (propertyLines.length > 0) {
900
+ for (const line of propertyLines) {
901
+ objectProperties += "\n" + line;
1332
902
  }
1333
- const notObjectBranch = strict
1334
- ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1335
- : ` return {} as unknown as ${typeName};`;
1336
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1337
- return `export const ${functionName} = (input: unknown): ${typeName} => {
903
+ }
904
+ const notObjectBranch = strict ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);` : ` return {} as unknown as ${typeName};`;
905
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
906
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1338
907
  if (!isObject(input)) {
1339
908
  ${notObjectBranch}
1340
909
  }${keywordPrelude}
@@ -1350,162 +919,135 @@ ${objectProperties}
1350
919
  return result;
1351
920
  };`;
1352
921
  };
1353
- /**
1354
- * Generates a parser for schemas with additionalProperties.
1355
- */
1356
922
  const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1357
- const functionName = generateParserName(typeName);
1358
- const additionalProps = schema.additionalProperties;
1359
- // Check if additionalProps is defined before using it
1360
- if (!additionalProps) {
1361
- return generateEmptyObjectParser(typeName, strict, schema);
1362
- }
1363
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1364
- // If additionalProperties is a $ref and useRefImports is true, generate a loop
1365
- if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
1366
- const ref = additionalProps.$ref;
1367
- const parserName = generateParserName(refToName(ref, suffix));
1368
- if (strict) {
1369
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${parserName}) as ${typeName};\n};`;
1370
- }
1371
- return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
1372
- }
1373
- // Handle additionalProperties with a known type by generating inline validation
1374
- if (isSchemaObject(additionalProps) && hasType(additionalProps)) {
1375
- const inlineParser = generateInlineValueParser(additionalProps, strict, `[${typeName}]`);
1376
- if (inlineParser) {
1377
- if (strict) {
1378
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`;
1379
- }
1380
- return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
1381
- }
1382
- }
1383
- // Otherwise, just validate the input type and shallow copy
923
+ const functionName = generateParserName(typeName);
924
+ const additionalProps = schema.additionalProperties;
925
+ if (!additionalProps) {
926
+ return generateEmptyObjectParser(typeName, strict, schema);
927
+ }
928
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
929
+ if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
930
+ const ref = additionalProps.$ref;
931
+ const parserName = generateParserName(refToName(ref, suffix));
1384
932
  if (strict) {
1385
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return { ...input } as ${typeName};\n};`;
1386
- }
1387
- return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
1388
- };
1389
- /**
1390
- * Generates an inline arrow function that validates a single value based on
1391
- * the additionalProperties schema type. Returns null if the type is not supported.
1392
- *
1393
- * Used to generate parsers for Record-like types where values have a known
1394
- * primitive or array type (e.g. Record<string, string> or Record<string, string[]>).
1395
- */
1396
- const generateInlineValueParser = (schema, strict = false, label = '') => {
1397
- if (!isSchemaObject(schema) || !hasType(schema)) {
1398
- return null;
933
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
934
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}
935
+ return validateRecord(input, ${parserName}) as ${typeName};
936
+ };`;
1399
937
  }
1400
- // Strict mode must *throw* on a wrong-typed record value, not repair it — the
1401
- // coercing branch below would silently turn `{ a: 'x' }` into `{ a: 0 }` for a
1402
- // `Record<string, number>`, which strict mode is documented never to do.
1403
- if (strict) {
1404
- const err = (t) => `throw new Error(${JSON.stringify(`${label} record value must be ${t}`)})`;
1405
- switch (schema.type) {
1406
- case 'string':
1407
- return `(value: unknown) => { if (typeof value !== "string") ${err('string')}; return value; }`;
1408
- case 'number':
1409
- return `(value: unknown) => { if (typeof value !== "number") ${err('number')}; return value; }`;
1410
- case 'integer':
1411
- return `(value: unknown) => { if (typeof value !== "number" || !Number.isInteger(value)) ${err('integer')}; return value; }`;
1412
- case 'boolean':
1413
- return `(value: unknown) => { if (typeof value !== "boolean") ${err('boolean')}; return value; }`;
1414
- case 'array':
1415
- return `(value: unknown) => { if (!Array.isArray(value)) ${err('array')}; return value; }`;
1416
- default:
1417
- return null;
1418
- }
938
+ return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
939
+ }
940
+ if (isSchemaObject(additionalProps) && hasType(additionalProps)) {
941
+ const inlineParser = generateInlineValueParser(additionalProps, strict, `[${typeName}]`);
942
+ if (inlineParser) {
943
+ if (strict) {
944
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
945
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}
946
+ return validateRecord(input, ${inlineParser}) as ${typeName};
947
+ };`;
948
+ }
949
+ return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
1419
950
  }
951
+ }
952
+ if (strict) {
953
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
954
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}
955
+ return { ...input } as ${typeName};
956
+ };`;
957
+ }
958
+ return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
959
+ };
960
+ const generateInlineValueParser = (schema, strict = false, label = "") => {
961
+ if (!isSchemaObject(schema) || !hasType(schema)) {
962
+ return null;
963
+ }
964
+ if (strict) {
965
+ const err = (t) => `throw new Error(${JSON.stringify(`${label} record value must be ${t}`)})`;
1420
966
  switch (schema.type) {
1421
- case 'string':
1422
- return '(value: unknown) => typeof value === "string" ? value : ""';
1423
- case 'number':
1424
- return '(value: unknown) => typeof value === "number" ? value : 0';
1425
- case 'integer':
1426
- // `integer` rejects non-integral numbers; a bare typeof accepts `1.5`,
1427
- // matching every other integer site in this package (and strict mode).
1428
- return '(value: unknown) => typeof value === "number" && Number.isInteger(value) ? value : 0';
1429
- case 'boolean':
1430
- return '(value: unknown) => typeof value === "boolean" ? value : false';
1431
- case 'array':
1432
- return '(value: unknown) => Array.isArray(value) ? value : []';
1433
- default:
1434
- return null;
967
+ case "string":
968
+ return `(value: unknown) => { if (typeof value !== "string") ${err("string")}; return value; }`;
969
+ case "number":
970
+ return `(value: unknown) => { if (typeof value !== "number") ${err("number")}; return value; }`;
971
+ case "integer":
972
+ return `(value: unknown) => { if (typeof value !== "number" || !Number.isInteger(value)) ${err("integer")}; return value; }`;
973
+ case "boolean":
974
+ return `(value: unknown) => { if (typeof value !== "boolean") ${err("boolean")}; return value; }`;
975
+ case "array":
976
+ return `(value: unknown) => { if (!Array.isArray(value)) ${err("array")}; return value; }`;
977
+ default:
978
+ return null;
1435
979
  }
980
+ }
981
+ switch (schema.type) {
982
+ case "string":
983
+ return '(value: unknown) => typeof value === "string" ? value : ""';
984
+ case "number":
985
+ return '(value: unknown) => typeof value === "number" ? value : 0';
986
+ case "integer":
987
+ return '(value: unknown) => typeof value === "number" && Number.isInteger(value) ? value : 0';
988
+ case "boolean":
989
+ return '(value: unknown) => typeof value === "boolean" ? value : false';
990
+ case "array":
991
+ return "(value: unknown) => Array.isArray(value) ? value : []";
992
+ default:
993
+ return null;
994
+ }
1436
995
  };
1437
- /**
1438
- * Generates a parser for schemas with patternProperties.
1439
- * Handles both patternProperties and specification-extensions (x- prefix).
1440
- */
1441
996
  const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1442
- const functionName = generateParserName(typeName);
1443
- if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
1444
- return generateEmptyObjectParser(typeName, strict, schema);
1445
- }
1446
- const patternProps = schema.patternProperties;
1447
- // Find the first pattern and its schema
1448
- const patterns = Object.entries(patternProps);
1449
- if (patterns.length === 0) {
1450
- return generateEmptyObjectParser(typeName, strict, schema);
1451
- }
1452
- const [pattern, patternSchema] = patterns[0];
1453
- let patternAssignment = '(result as Record<string, unknown>)[key] = value;';
1454
- // The value-expression form, used by the strictKeys path (which builds from
1455
- // `{}`) to emit a prototype-safe assignment. `null` when no value is allowed.
1456
- let patternValueExpr = 'value';
1457
- // Use imported parser when pattern schema points to a $ref.
1458
- if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
1459
- const ref = patternSchema.$ref;
1460
- const parserName = generateParserName(refToName(ref, suffix));
1461
- patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1462
- patternValueExpr = `${parserName}(value)`;
1463
- }
1464
- else if (patternSchema === false) {
1465
- // `false` means no values are allowed for matching keys.
1466
- patternAssignment = '';
1467
- patternValueExpr = null;
997
+ const functionName = generateParserName(typeName);
998
+ if (!("patternProperties" in schema) || typeof schema.patternProperties !== "object") {
999
+ return generateEmptyObjectParser(typeName, strict, schema);
1000
+ }
1001
+ const patternProps = schema.patternProperties;
1002
+ const patterns = Object.entries(patternProps);
1003
+ if (patterns.length === 0) {
1004
+ return generateEmptyObjectParser(typeName, strict, schema);
1005
+ }
1006
+ const [pattern, patternSchema] = patterns[0];
1007
+ let patternAssignment = "(result as Record<string, unknown>)[key] = value;";
1008
+ let patternValueExpr = "value";
1009
+ if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
1010
+ const ref = patternSchema.$ref;
1011
+ const parserName = generateParserName(refToName(ref, suffix));
1012
+ patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1013
+ patternValueExpr = `${parserName}(value)`;
1014
+ } else if (patternSchema === false) {
1015
+ patternAssignment = "";
1016
+ patternValueExpr = null;
1017
+ }
1018
+ const escapedPattern = escapeRegexPattern(pattern);
1019
+ const notObjectBranch = strict ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);` : ` return {} as unknown as ${typeName};`;
1020
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
1021
+ const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1022
+ if (strictKeys) {
1023
+ const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1024
+ if (patternValueExpr) {
1025
+ loopLines.push(` const value = input[key];`);
1026
+ loopLines.push(` ${safeResultAssign("key", patternValueExpr)}`);
1027
+ }
1028
+ loopLines.push(` continue;`);
1029
+ loopLines.push(` }`);
1030
+ const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1031
+ if (keepConditions.length > 0) {
1032
+ loopLines.push(` if (${keepConditions.join(" || ")}) {`);
1033
+ loopLines.push(` ${safeResultAssign("key", "input[key]")}`);
1034
+ loopLines.push(` continue;`);
1035
+ loopLines.push(` }`);
1468
1036
  }
1469
- // Escape the pattern for safe inclusion in generated code
1470
- const escapedPattern = escapeRegexPattern(pattern);
1471
- const notObjectBranch = strict
1472
- ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1473
- : ` return {} as unknown as ${typeName};`;
1474
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1475
- // With `additionalProperties: false`, only keys matching a pattern survive:
1476
- // others are rejected (strict) or stripped (coerce). Start from an empty
1477
- // object rather than spreading every input key.
1478
- const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1479
- if (strictKeys) {
1480
- const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1481
- if (patternValueExpr) {
1482
- loopLines.push(` const value = input[key];`);
1483
- loopLines.push(` ${safeResultAssign('key', patternValueExpr)}`);
1484
- }
1485
- loopLines.push(` continue;`);
1486
- loopLines.push(` }`);
1487
- const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1488
- if (keepConditions.length > 0) {
1489
- loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1490
- loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1491
- loopLines.push(` continue;`);
1492
- loopLines.push(` }`);
1493
- }
1494
- if (strict)
1495
- loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
1496
- return `export const ${functionName} = (input: unknown): ${typeName} => {
1037
+ if (strict)
1038
+ loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
1039
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1497
1040
  if (!isObject(input)) {
1498
1041
  ${notObjectBranch}
1499
1042
  }${keywordPrelude}
1500
1043
  const result = {} as unknown as ${typeName};
1501
1044
  for (const key in input) {
1502
- ${loopLines.join('\n')}
1045
+ ${loopLines.join("\n")}
1503
1046
  }
1504
1047
  return result;
1505
1048
  };`;
1506
- }
1507
- // Generate a parser that handles both pattern matching and x- extensions
1508
- return `export const ${functionName} = (input: unknown): ${typeName} => {
1049
+ }
1050
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1509
1051
  if (!isObject(input)) {
1510
1052
  ${notObjectBranch}
1511
1053
  }${keywordPrelude}
@@ -1521,117 +1063,91 @@ ${notObjectBranch}
1521
1063
  return result;
1522
1064
  };`;
1523
1065
  };
1524
- /**
1525
- * Generates a parser for conditional schemas with if/then/else.
1526
- * This always generates parser calls regardless of useRefImports setting,
1527
- * because conditional logic requires delegating to the appropriate parser.
1528
- */
1529
1066
  const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false) => {
1530
- const functionName = generateParserName(typeName);
1531
- // Extract the condition and branches
1532
- const ifSchema = schema.if;
1533
- const thenSchema = schema.then;
1534
- const elseSchema = schema.else;
1535
- // Check if branches are defined and have $ref
1536
- const thenHasRef = thenSchema && isSchemaObject(thenSchema) && hasRef(thenSchema);
1537
- const elseHasRef = elseSchema && isSchemaObject(elseSchema) && hasRef(elseSchema);
1538
- if (!thenHasRef || !elseHasRef) {
1539
- // Non-$ref branches (e.g. if/then/else providing defaults for properties).
1540
- // Flatten all three branches into a single object schema and generate a
1541
- // regular object parser from the merged property set.
1542
- const mergedSchema = getConditionalObjectSchema(schema);
1543
- if (mergedSchema) {
1544
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1545
- }
1546
- return generateEmptyObjectParser(typeName);
1547
- }
1548
- // Check if the condition is checking for $ref property
1549
- const isRefCondition = ifSchema &&
1550
- isSchemaObject(ifSchema) &&
1551
- 'required' in ifSchema &&
1552
- Array.isArray(ifSchema.required) &&
1553
- ifSchema.required.includes('$ref');
1554
- if (!isRefCondition) {
1555
- // Condition is not a $ref check — flatten into an object parser
1556
- const mergedSchema = getConditionalObjectSchema(schema);
1557
- if (mergedSchema) {
1558
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1559
- }
1560
- return generateEmptyObjectParser(typeName);
1067
+ const functionName = generateParserName(typeName);
1068
+ const ifSchema = schema.if;
1069
+ const thenSchema = schema.then;
1070
+ const elseSchema = schema.else;
1071
+ const thenHasRef = thenSchema && isSchemaObject(thenSchema) && hasRef(thenSchema);
1072
+ const elseHasRef = elseSchema && isSchemaObject(elseSchema) && hasRef(elseSchema);
1073
+ if (!thenHasRef || !elseHasRef) {
1074
+ const mergedSchema = getConditionalObjectSchema(schema);
1075
+ if (mergedSchema) {
1076
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1077
+ }
1078
+ return generateEmptyObjectParser(typeName);
1079
+ }
1080
+ const isRefCondition = ifSchema && isSchemaObject(ifSchema) && "required" in ifSchema && Array.isArray(ifSchema.required) && ifSchema.required.includes("$ref");
1081
+ if (!isRefCondition) {
1082
+ const mergedSchema = getConditionalObjectSchema(schema);
1083
+ if (mergedSchema) {
1084
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1561
1085
  }
1562
- const thenRef = thenSchema.$ref;
1563
- const elseRef = elseSchema.$ref;
1564
- const thenParserName = generateParserName(refToName(thenRef, suffix));
1565
- const elseParserName = generateParserName(refToName(elseRef, suffix));
1566
- const thenTypeName = refToName(thenRef, suffix);
1567
- return `export const ${functionName} = (input: unknown): ${typeName} | ${thenTypeName} =>
1086
+ return generateEmptyObjectParser(typeName);
1087
+ }
1088
+ const thenRef = thenSchema.$ref;
1089
+ const elseRef = elseSchema.$ref;
1090
+ const thenParserName = generateParserName(refToName(thenRef, suffix));
1091
+ const elseParserName = generateParserName(refToName(elseRef, suffix));
1092
+ const thenTypeName = refToName(thenRef, suffix);
1093
+ return `export const ${functionName} = (input: unknown): ${typeName} | ${thenTypeName} =>
1568
1094
  hasRef(input) ? ${thenParserName}(input) : ${elseParserName}(input)
1569
1095
  `;
1570
1096
  };
1571
- /**
1572
- * Builds an object schema from conditional if/then keywords when the schema does not
1573
- * declare type: "object". Flattens if/then/else property sets into a single object schema.
1574
- */
1575
1097
  const getConditionalObjectSchema = (schema) => {
1576
- if (!isSchemaObject(schema)) {
1577
- return null;
1578
- }
1579
- if (!('if' in schema) || !('then' in schema)) {
1580
- return null;
1581
- }
1582
- const ifSchema = schema.if;
1583
- const thenSchema = schema.then;
1584
- const elseSchema = 'else' in schema ? schema.else : undefined;
1585
- if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) {
1586
- return null;
1587
- }
1588
- const ifProperties = ifSchema.properties;
1589
- const thenProperties = thenSchema.properties;
1590
- const elseProperties = elseSchema && isSchemaObject(elseSchema) ? elseSchema.properties : undefined;
1591
- const hasIfProperties = ifProperties && typeof ifProperties === 'object';
1592
- const hasThenProperties = thenProperties && typeof thenProperties === 'object';
1593
- const hasElseProperties = elseProperties && typeof elseProperties === 'object';
1594
- if (!hasIfProperties && !hasThenProperties && !hasElseProperties) {
1595
- return null;
1596
- }
1597
- const required = new Set();
1598
- if (Array.isArray(ifSchema.required)) {
1599
- for (const key of ifSchema.required) {
1600
- required.add(key);
1601
- }
1098
+ if (!isSchemaObject(schema)) {
1099
+ return null;
1100
+ }
1101
+ if (!("if" in schema) || !("then" in schema)) {
1102
+ return null;
1103
+ }
1104
+ const ifSchema = schema.if;
1105
+ const thenSchema = schema.then;
1106
+ const elseSchema = "else" in schema ? schema.else : void 0;
1107
+ if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) {
1108
+ return null;
1109
+ }
1110
+ const ifProperties = ifSchema.properties;
1111
+ const thenProperties = thenSchema.properties;
1112
+ const elseProperties = elseSchema && isSchemaObject(elseSchema) ? elseSchema.properties : void 0;
1113
+ const hasIfProperties = ifProperties && typeof ifProperties === "object";
1114
+ const hasThenProperties = thenProperties && typeof thenProperties === "object";
1115
+ const hasElseProperties = elseProperties && typeof elseProperties === "object";
1116
+ if (!hasIfProperties && !hasThenProperties && !hasElseProperties) {
1117
+ return null;
1118
+ }
1119
+ const required = /* @__PURE__ */ new Set();
1120
+ if (Array.isArray(ifSchema.required)) {
1121
+ for (const key of ifSchema.required) {
1122
+ required.add(key);
1602
1123
  }
1603
- // If the `if` condition checks for a `const` value on a property, that property
1604
- // is effectively required (the schema only applies when the condition is true).
1605
- if (hasIfProperties && ifProperties && typeof ifProperties === 'object') {
1606
- for (const [key, propSchema] of Object.entries(ifProperties)) {
1607
- if (isSchemaObject(propSchema) && hasConst(propSchema)) {
1608
- required.add(key);
1609
- }
1610
- }
1124
+ }
1125
+ if (hasIfProperties && ifProperties && typeof ifProperties === "object") {
1126
+ for (const [key, propSchema] of Object.entries(ifProperties)) {
1127
+ if (isSchemaObject(propSchema) && hasConst(propSchema)) {
1128
+ required.add(key);
1129
+ }
1611
1130
  }
1612
- if (Array.isArray(thenSchema.required)) {
1613
- for (const key of thenSchema.required) {
1614
- required.add(key);
1615
- }
1131
+ }
1132
+ if (Array.isArray(thenSchema.required)) {
1133
+ for (const key of thenSchema.required) {
1134
+ required.add(key);
1616
1135
  }
1617
- return {
1618
- type: 'object',
1619
- properties: {
1620
- // else properties first so then properties take precedence on overlap
1621
- ...(hasElseProperties ? elseProperties : {}),
1622
- ...(hasIfProperties ? ifProperties : {}),
1623
- ...(hasThenProperties ? thenProperties : {}),
1624
- },
1625
- ...(required.size > 0 ? { required: Array.from(required) } : {}),
1626
- };
1136
+ }
1137
+ return {
1138
+ type: "object",
1139
+ properties: {
1140
+ // else properties first so then properties take precedence on overlap
1141
+ ...hasElseProperties ? elseProperties : {},
1142
+ ...hasIfProperties ? ifProperties : {},
1143
+ ...hasThenProperties ? thenProperties : {}
1144
+ },
1145
+ ...required.size > 0 ? { required: Array.from(required) } : {}
1146
+ };
1627
1147
  };
1628
- /**
1629
- * Generates a parser for SchemaObject that validates all JSON Schema 2020-12 properties.
1630
- * This handles the special case where a schema can be any valid JSON Schema.
1631
- */
1632
1148
  const generateSchemaObjectParser = (typeName) => {
1633
- const functionName = generateParserName(typeName);
1634
- return `export const ${functionName} = (input: unknown): ${typeName} => {
1149
+ const functionName = generateParserName(typeName);
1150
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
1635
1151
  if (typeof input === 'boolean') {
1636
1152
  return input as ${typeName};
1637
1153
  }
@@ -1643,251 +1159,156 @@ const generateSchemaObjectParser = (typeName) => {
1643
1159
  return input as ${typeName};
1644
1160
  };`;
1645
1161
  };
1646
- /**
1647
- * Determines the appropriate parser generation strategy for a schema.
1648
- */
1649
1162
  const selectParserStrategy = (schema, typeName, options) => {
1650
- const useRefImports = options?.useRefImports ?? false;
1651
- const logWarnings = options?.logWarnings ?? false;
1652
- const strict = options?.strict ?? false;
1653
- const stripUnknown = options?.stripUnknown ?? false;
1654
- const caseInsensitive = options?.caseInsensitive ?? false;
1655
- const suffix = options?.typeSuffix ?? '';
1656
- // Special case for the self-referential JSON Schema meta-schema type (e.g.
1657
- // `Schema` / `SchemaObject`) - it can be any JSON Schema. This is an OpenAPI
1658
- // heuristic for a `$defs`/`components.schemas` entry named `schema`; it must
1659
- // never fire for the root document, whose name is user-derived (a `schema.json`
1660
- // file naturally yields the root type `Schema`) and would otherwise collapse
1661
- // to a validation-free pass-through parser.
1662
- if (!options?.isRoot && typeName === `Schema${suffix}`) {
1663
- return generateSchemaObjectParser(typeName);
1664
- }
1665
- const isObjectLikeSchema = isObjectSchema(schema) ||
1666
- (isSchemaObject(schema) && ('patternProperties' in schema || 'additionalProperties' in schema));
1667
- // Handle non-object schemas with type-appropriate validation
1668
- if (!isObjectLikeSchema && !isSchemaObject(schema)) {
1669
- return generateNonObjectParser(typeName, schema, strict, {
1670
- useRefImports,
1671
- suffix,
1672
- stripUnknown,
1673
- caseInsensitive,
1674
- logWarnings,
1675
- ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1676
- ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
1677
- });
1678
- }
1679
- // Handle schemas with both properties AND patternProperties.
1680
- // This generates a parser that handles known properties and also iterates
1681
- // pattern-matched keys (e.g. responses with "default" + "200", "4XX").
1682
- if (hasProperties(schema) && isSchemaObject(schema) && 'patternProperties' in schema) {
1683
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive);
1684
- }
1685
- // Handle schemas that have explicit properties — generate a full object parser.
1686
- // This intentionally runs before if/then checks so that schemas with both
1687
- // properties AND conditional keywords use all declared properties rather than
1688
- // only the if/then fragment.
1689
- if (hasProperties(schema)) {
1690
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive);
1691
- }
1692
- // Handle conditional schemas (if/then/else) for schemas without explicit properties.
1693
- if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
1694
- return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1695
- }
1696
- // Handle conditional schemas that only define if/then object fragments.
1697
- // We flatten the fragments into a regular object parser.
1698
- const conditionalObjectSchema = getConditionalObjectSchema(schema);
1699
- if (conditionalObjectSchema) {
1700
- return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1701
- }
1702
- // Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
1703
- if (!isObjectLikeSchema) {
1704
- return generateNonObjectParser(typeName, schema, strict, {
1705
- useRefImports,
1706
- suffix,
1707
- stripUnknown,
1708
- caseInsensitive,
1709
- logWarnings,
1710
- ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1711
- ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
1712
- });
1713
- }
1714
- // Handle schemas with patternProperties (but no properties)
1715
- if ('patternProperties' in schema) {
1716
- return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1717
- }
1718
- // Handle schemas with additionalProperties as true or false (but no properties)
1719
- if ('additionalProperties' in schema && !hasProperties(schema)) {
1720
- const additionalProps = schema.additionalProperties;
1721
- // If additionalProperties is true or false, validate it is an object
1722
- if (additionalProps === true || additionalProps === false) {
1723
- return generateEmptyObjectParser(typeName, strict, schema);
1724
- }
1725
- // Otherwise, handle as a schema (could be a $ref or object schema)
1726
- return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1727
- }
1728
- // Handle empty object schemas
1729
- if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
1730
- return generateEmptyObjectParser(typeName, strict, schema);
1163
+ const useRefImports = options?.useRefImports ?? false;
1164
+ const logWarnings = options?.logWarnings ?? false;
1165
+ const strict = options?.strict ?? false;
1166
+ const stripUnknown = options?.stripUnknown ?? false;
1167
+ const caseInsensitive = options?.caseInsensitive ?? false;
1168
+ const suffix = options?.typeSuffix ?? "";
1169
+ if (!options?.isRoot && typeName === `Schema${suffix}`) {
1170
+ return generateSchemaObjectParser(typeName);
1171
+ }
1172
+ const isObjectLikeSchema = isObjectSchema(schema) || isSchemaObject(schema) && ("patternProperties" in schema || "additionalProperties" in schema);
1173
+ if (!isObjectLikeSchema && !isSchemaObject(schema)) {
1174
+ return generateNonObjectParser(typeName, schema, strict, {
1175
+ useRefImports,
1176
+ suffix,
1177
+ stripUnknown,
1178
+ caseInsensitive,
1179
+ logWarnings,
1180
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1181
+ ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1182
+ });
1183
+ }
1184
+ if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
1185
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive);
1186
+ }
1187
+ if (hasProperties(schema)) {
1188
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource);
1189
+ }
1190
+ if (isSchemaObject(schema) && "if" in schema && "then" in schema && "else" in schema) {
1191
+ return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1192
+ }
1193
+ const conditionalObjectSchema = getConditionalObjectSchema(schema);
1194
+ if (conditionalObjectSchema) {
1195
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1196
+ }
1197
+ if (!isObjectLikeSchema) {
1198
+ return generateNonObjectParser(typeName, schema, strict, {
1199
+ useRefImports,
1200
+ suffix,
1201
+ stripUnknown,
1202
+ caseInsensitive,
1203
+ logWarnings,
1204
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1205
+ ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1206
+ });
1207
+ }
1208
+ if ("patternProperties" in schema) {
1209
+ return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1210
+ }
1211
+ if ("additionalProperties" in schema && !hasProperties(schema)) {
1212
+ const additionalProps = schema.additionalProperties;
1213
+ if (additionalProps === true || additionalProps === false) {
1214
+ return generateEmptyObjectParser(typeName, strict, schema);
1731
1215
  }
1732
- // Default fallback - validate it is an object since we passed the isObjectSchema check
1216
+ return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1217
+ }
1218
+ if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
1733
1219
  return generateEmptyObjectParser(typeName, strict, schema);
1220
+ }
1221
+ return generateEmptyObjectParser(typeName, strict, schema);
1734
1222
  };
1735
- /**
1736
- * Generates a safe parser as an arrow function that never throws.
1737
- * The parser provides default values for all missing or invalid fields.
1738
- *
1739
- * When useRefImports is enabled, properties with $ref will call the imported
1740
- * parser function (e.g., parseContact(input?.contact)) instead of inlining
1741
- * the resolved schema's validation logic. Array properties whose items are a
1742
- * $ref will map each element through the imported parser.
1743
- */
1744
- export const generateParserFunction = (schema, typeName, options) => {
1745
- // Strict parsers promise to throw on violations, so any keyword we cannot
1746
- // enforce must fail loudly at generation rather than emit a permissive parser.
1747
- // Coercing parsers are documented to repair rather than reject, so the guard
1748
- // does not apply to them.
1749
- if (options?.strict)
1750
- assertNoUnsupportedKeywords(schema, typeName);
1751
- return selectParserStrategy(schema, typeName, options);
1223
+ const generateParserFunction = (schema, typeName, options) => {
1224
+ if (options?.strict)
1225
+ assertNoUnsupportedKeywords(schema, typeName);
1226
+ return selectParserStrategy(schema, typeName, options);
1752
1227
  };
1753
- /**
1754
- * Returns the predicate function name for a generated shape validator.
1755
- * @example shapeValidatorFunctionName('CustomerObject') // 'validateCustomerObjectShape'
1756
- */
1757
- export const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
1758
- /**
1759
- * Generates a `validate{TypeName}Shape(input)` predicate that returns true
1760
- * iff `input` already matches the shape produced by the parser's fast path —
1761
- * i.e. the parser would return `{ ...input } as TypeName` without coercion.
1762
- *
1763
- * Parents call this predicate to fast-path through nested ref properties
1764
- * (and arrays of refs) without recursing into their parser.
1765
- *
1766
- * Returns a stub that always returns `false` for schemas that cannot be
1767
- * predicated (composition, conditionals, pattern properties, complex refs
1768
- * in additionalProperties). The stub is safe: parents calling it will fall
1769
- * through to the slow path, matching the pre-deep-fast-path behavior.
1770
- */
1771
- export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '', exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES) => {
1772
- const fnName = shapeValidatorName(typeName);
1773
- const exportPrefix = exported ? 'export ' : '';
1774
- const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1775
- if (!isSchemaObject(schema))
1776
- return stub;
1777
- // Keywords the parser's fast path cannot mirror (contains, dependentRequired,
1778
- // dependentSchemas, propertyNames) make a bare shape match insufficient: a
1779
- // parent that fast-pathed through this predicate would skip their enforcement,
1780
- // so stub out and force the parent's slow path (which calls the real parser).
1781
- if (hasFastPathBlockingKeyword(schema))
1782
- return stub;
1783
- // An alias definition (a bare `$ref` with no shape of its own) delegates to
1784
- // the referenced type's validator. Guarded against self-reference.
1785
- if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
1786
- const refName = refToName(schema.$ref, suffix);
1787
- if (refName !== typeName) {
1788
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${shapeValidatorName(refName)}(input);`;
1789
- }
1790
- return stub;
1791
- }
1792
- // A pure union definition (file-level oneOf/anyOf without properties) gets a
1793
- // real membership predicate when every branch is checkable. A recursive
1794
- // union (e.g. `expr` whose branches $ref `expr` itself) works because the
1795
- // branch checks call this very validator by name at runtime. Skipped under
1796
- // stripUnknown: the branch checks carry no known-keys terms, so a `true`
1797
- // could not guarantee the parser's strip build would be a no-op.
1798
- if (!hasProperties(schema) && !('patternProperties' in schema) && !('if' in schema) && !stripUnknown) {
1799
- const branches = getUnionBranches(schema);
1800
- if (branches) {
1801
- const check = generateUnionCheck('input', branches, useRefImports, suffix);
1802
- if (check !== null) {
1803
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
1804
- }
1805
- }
1806
- return stub;
1228
+ const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
1229
+ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES) => {
1230
+ const fnName = shapeValidatorName(typeName);
1231
+ const exportPrefix = exported ? "export " : "";
1232
+ const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1233
+ if (!isSchemaObject(schema))
1234
+ return stub;
1235
+ if (hasFastPathBlockingKeyword(schema))
1236
+ return stub;
1237
+ if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
1238
+ const refName = refToName(schema.$ref, suffix);
1239
+ if (refName !== typeName) {
1240
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${shapeValidatorName(refName)}(input);`;
1241
+ }
1242
+ return stub;
1243
+ }
1244
+ if (!hasProperties(schema) && !("patternProperties" in schema) && !("if" in schema) && !stripUnknown) {
1245
+ const branches = getUnionBranches(schema);
1246
+ if (branches) {
1247
+ const check = generateUnionCheck("input", branches, useRefImports, suffix);
1248
+ if (check !== null) {
1249
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
1250
+ }
1251
+ }
1252
+ return stub;
1253
+ }
1254
+ if (!hasProperties(schema))
1255
+ return stub;
1256
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema || "patternProperties" in schema || "if" in schema || "then" in schema || "else" in schema) {
1257
+ return stub;
1258
+ }
1259
+ const schemaProps = schema.properties;
1260
+ const propertyKeys = Object.keys(schemaProps);
1261
+ const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
1262
+ const validatorStripKeys = hasStrictKeys(schema) || stripUnknown;
1263
+ const validatorKeyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
1264
+ const strictKeysGuard = validatorStripKeys && validatorKeyCount === null ? `
1265
+ if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
1266
+ const checks = [];
1267
+ for (const key of propertyKeys) {
1268
+ const propSchema = schemaProps[key];
1269
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
1270
+ return stub;
1807
1271
  }
1808
- if (!hasProperties(schema))
1809
- return stub;
1810
- // Composition / conditional schemas can match in many shapes — bail.
1811
- if (hasOneOf(schema) ||
1812
- hasAnyOf(schema) ||
1813
- hasAllOf(schema) ||
1814
- 'not' in schema ||
1815
- 'patternProperties' in schema ||
1816
- 'if' in schema ||
1817
- 'then' in schema ||
1818
- 'else' in schema) {
1819
- return stub;
1272
+ const accessor = safeAccessor("input", key);
1273
+ const isRequired = isPropertyRequired(key, schema);
1274
+ const subName = subTypeNames.get(key);
1275
+ let check = subName ? `${shapeValidatorName(subName)}(${accessor})` : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
1276
+ const itemSubName = subItemNames.get(key);
1277
+ if (check !== null && itemSubName) {
1278
+ check = `${check} && _every${itemSubName}(${accessor})`;
1820
1279
  }
1821
- const schemaProps = schema.properties;
1822
- const propertyKeys = Object.keys(schemaProps);
1823
- // Same deterministic naming as the parser's sub-parser generation, so the
1824
- // referenced private shape predicates exist in the same file.
1825
- const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
1826
- // When the parser strips extras (additionalProperties: false or stripUnknown),
1827
- // an input carrying an undeclared key is *not* fast-path eligible — the parser
1828
- // would strip it rather than return `{ ...input }` — so the shape only matches
1829
- // when every key is declared. With every declared property required, the
1830
- // no-extras test is the cheaper own-key count appended after the typed checks
1831
- // (which prove all N keys present); otherwise it is the `_hasOnlyKnownKeys`
1832
- // walk the parser emits under the same `stripKeys` condition.
1833
- const validatorStripKeys = hasStrictKeys(schema) || stripUnknown;
1834
- const validatorKeyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
1835
- const strictKeysGuard = validatorStripKeys && validatorKeyCount === null
1836
- ? `\n if (!_hasOnlyKnownKeys${typeName}(input)) return false;`
1837
- : '';
1838
- const checks = [];
1839
- for (const key of propertyKeys) {
1840
- const propSchema = schemaProps[key];
1841
- // Records of refs require iterating every value — too expensive for the fast path.
1842
- if (shouldUseRecordRefImport(propSchema, useRefImports)) {
1843
- return stub;
1844
- }
1845
- const accessor = safeAccessor('input', key);
1846
- const isRequired = isPropertyRequired(key, schema);
1847
- const subName = subTypeNames.get(key);
1848
- let check = subName
1849
- ? `${shapeValidatorName(subName)}(${accessor})`
1850
- : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
1851
- // Arrays of inline objects prove every element via the private item loop
1852
- // helper (emitted alongside the item sub-parser), matching the parser's
1853
- // fast-path guard.
1854
- const itemSubName = subItemNames.get(key);
1855
- if (check !== null && itemSubName) {
1856
- check = `${check} && _every${itemSubName}(${accessor})`;
1857
- }
1858
- if (check === null) {
1859
- return stub;
1860
- }
1861
- if (isRequired) {
1862
- checks.push(check);
1863
- }
1864
- else {
1865
- checks.push(`(${accessor} === undefined || ${check})`);
1866
- }
1280
+ if (check === null) {
1281
+ return stub;
1867
1282
  }
1868
- // The count form is sound only in conjunction with the typed checks above
1869
- // (they prove every declared key present) and only for plain objects (a
1870
- // crafted prototype could satisfy those checks through inherited
1871
- // properties), so it joins the chain last, prototype-guarded like the
1872
- // parser's own fast path.
1873
- if (validatorKeyCount !== null) {
1874
- checks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${validatorKeyCount}`);
1283
+ if (isRequired) {
1284
+ checks.push(check);
1285
+ } else {
1286
+ checks.push(`(${accessor} === undefined || ${check})`);
1875
1287
  }
1876
- if (checks.length === 0) {
1877
- if (strictKeysGuard) {
1878
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1288
+ }
1289
+ if (validatorKeyCount !== null) {
1290
+ checks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${validatorKeyCount}`);
1291
+ }
1292
+ if (checks.length === 0) {
1293
+ if (strictKeysGuard) {
1294
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1879
1295
  if (!isObject(input)) return false;${strictKeysGuard}
1880
1296
  return true;
1881
1297
  };`;
1882
- }
1883
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
1884
- }
1885
- let body = checks[0];
1886
- for (let i = 1; i < checks.length; i++) {
1887
- body += '\n && ' + checks[i];
1888
1298
  }
1889
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1299
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
1300
+ }
1301
+ let body = checks[0];
1302
+ for (let i = 1; i < checks.length; i++) {
1303
+ body += "\n && " + checks[i];
1304
+ }
1305
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1890
1306
  if (!isObject(input)) return false;${strictKeysGuard}
1891
1307
  return ${body};
1892
1308
  };`;
1893
1309
  };
1310
+ export {
1311
+ generateParserFunction,
1312
+ generateShapeValidator,
1313
+ shapeValidatorFunctionName
1314
+ };