@amritk/generate-parsers 0.6.3 → 0.7.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.
Files changed (56) hide show
  1. package/dist/generators/build-schema.d.ts +9 -10
  2. package/dist/generators/build-schema.d.ts.map +1 -0
  3. package/dist/generators/build-schema.js +118 -0
  4. package/dist/generators/generate-default-from-pattern.d.ts +1 -0
  5. package/dist/generators/generate-default-from-pattern.d.ts.map +1 -0
  6. package/dist/generators/generate-default-from-pattern.js +33 -0
  7. package/dist/generators/generate-discriminated-union-validation.d.ts +1 -0
  8. package/dist/generators/generate-discriminated-union-validation.d.ts.map +1 -0
  9. package/dist/generators/generate-discriminated-union-validation.js +38 -0
  10. package/dist/generators/generate-enum-check.d.ts +1 -0
  11. package/dist/generators/generate-enum-check.d.ts.map +1 -0
  12. package/dist/generators/generate-enum-check.js +8 -0
  13. package/dist/generators/generate-files.d.ts +2 -1
  14. package/dist/generators/generate-files.d.ts.map +1 -0
  15. package/dist/generators/generate-files.js +74 -0
  16. package/dist/generators/generate-non-discriminated-union-validation.d.ts +1 -0
  17. package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +1 -0
  18. package/dist/generators/generate-non-discriminated-union-validation.js +28 -0
  19. package/dist/generators/generate-parser-function.d.ts +1 -0
  20. package/dist/generators/generate-parser-function.d.ts.map +1 -0
  21. package/dist/generators/generate-parser-function.js +1070 -0
  22. package/dist/generators/generate-schema-checks.d.ts +1 -0
  23. package/dist/generators/generate-schema-checks.d.ts.map +1 -0
  24. package/dist/generators/generate-schema-checks.js +99 -0
  25. package/dist/generators/generate-strict-assertion.d.ts +1 -0
  26. package/dist/generators/generate-strict-assertion.d.ts.map +1 -0
  27. package/dist/generators/generate-strict-assertion.js +179 -0
  28. package/dist/generators/generate-validation-expression.d.ts +1 -0
  29. package/dist/generators/generate-validation-expression.d.ts.map +1 -0
  30. package/dist/generators/generate-validation-expression.js +267 -0
  31. package/dist/helpers/apply-schema-extensions.d.ts +2 -1
  32. package/dist/helpers/apply-schema-extensions.d.ts.map +1 -0
  33. package/dist/helpers/apply-schema-extensions.js +51 -0
  34. package/dist/helpers/collect-helpers.d.ts +1 -0
  35. package/dist/helpers/collect-helpers.d.ts.map +1 -0
  36. package/dist/helpers/collect-helpers.js +48 -0
  37. package/dist/helpers/collect-imports.d.ts +1 -0
  38. package/dist/helpers/collect-imports.d.ts.map +1 -0
  39. package/dist/helpers/collect-imports.js +216 -0
  40. package/dist/helpers/find-discriminator.d.ts +1 -0
  41. package/dist/helpers/find-discriminator.d.ts.map +1 -0
  42. package/dist/helpers/find-discriminator.js +33 -0
  43. package/dist/helpers/get-default-value.d.ts +1 -0
  44. package/dist/helpers/get-default-value.d.ts.map +1 -0
  45. package/dist/helpers/get-default-value.js +74 -0
  46. package/dist/helpers/get-discriminator-value.d.ts +1 -0
  47. package/dist/helpers/get-discriminator-value.d.ts.map +1 -0
  48. package/dist/helpers/get-discriminator-value.js +19 -0
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +1 -2783
  52. package/dist/types/schema-extensions.d.ts +1 -0
  53. package/dist/types/schema-extensions.d.ts.map +1 -0
  54. package/dist/types/schema-extensions.js +0 -0
  55. package/package.json +6 -6
  56. package/src/generators/build-schema.ts +22 -173
@@ -0,0 +1,1070 @@
1
+ import { refToName } from '@amritk/helpers/ref-to-name';
2
+ import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
3
+ import { hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
4
+ import { getDefaultValue } from '../helpers/get-default-value.js';
5
+ import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
6
+ import { generateValidationExpression } from './generate-validation-expression.js';
7
+ /**
8
+ * Generates the parser function name from a type name.
9
+ * Converts "UserObject" to "parseUserObject".
10
+ */
11
+ const generateParserName = (typeName) => {
12
+ return `parse${typeName}`;
13
+ };
14
+ /**
15
+ * Checks if a property is required based on the schema's required array.
16
+ */
17
+ const isPropertyRequired = (key, schema) => {
18
+ if (!isSchemaObject(schema)) {
19
+ return false;
20
+ }
21
+ if (!('required' in schema) || !Array.isArray(schema.required)) {
22
+ return false;
23
+ }
24
+ return schema.required.includes(key);
25
+ };
26
+ /**
27
+ * Generates a parser call expression for a required $ref property.
28
+ * Example: parseContact(input.contact)
29
+ */
30
+ const generateRequiredRefCall = (key, ref, suffix) => {
31
+ const acc = safeAccessor('input', key);
32
+ const parserName = generateParserName(refToName(ref, suffix));
33
+ return `${parserName}(${acc})`;
34
+ };
35
+ /**
36
+ * Generates a parser call expression for an optional $ref property.
37
+ * Example: ...(input.contact && { contact: parseContact(input.contact) })
38
+ */
39
+ const generateOptionalRefCall = (key, ref, suffix) => {
40
+ const acc = safeAccessor('input', key);
41
+ const parserName = generateParserName(refToName(ref, suffix));
42
+ return `...(${acc} && { ${safeKey(key)}: ${parserName}(${acc}) })`;
43
+ };
44
+ /**
45
+ * Generates a validateArray call for required array properties with $ref items.
46
+ * Example: validateArray(input.contacts, parseContact)
47
+ */
48
+ const generateRequiredArrayRefCall = (key, ref, suffix) => {
49
+ const parserName = generateParserName(refToName(ref, suffix));
50
+ return `validateArray(${safeAccessor('input', key)}, ${parserName})`;
51
+ };
52
+ /**
53
+ * Generates a validateArray call for optional array properties with $ref items.
54
+ * Example: ...(input.contacts && { contacts: validateArray(input.contacts, parseContact) })
55
+ */
56
+ const generateOptionalArrayRefCall = (key, ref, suffix) => {
57
+ const parserName = generateParserName(refToName(ref, suffix));
58
+ const acc = safeAccessor('input', key);
59
+ return `...(${acc} && { ${safeKey(key)}: validateArray(${acc}, ${parserName}) })`;
60
+ };
61
+ /**
62
+ * Generates a validateRecord call for required object properties with additionalProperties $ref.
63
+ * Example: validateRecord(input.responses, parseResponse)
64
+ */
65
+ const generateRequiredRecordRefCall = (key, ref, suffix) => {
66
+ const acc = safeAccessor('input', key);
67
+ const parserName = generateParserName(refToName(ref, suffix));
68
+ return `validateRecord(${acc}, ${parserName})`;
69
+ };
70
+ /**
71
+ * Generates a validateRecord call for optional object properties with additionalProperties $ref.
72
+ * Example: ...(input.responses && { responses: validateRecord(input.responses, parseResponse) })
73
+ */
74
+ const generateOptionalRecordRefCall = (key, ref, suffix) => {
75
+ const acc = safeAccessor('input', key);
76
+ const parserName = generateParserName(refToName(ref, suffix));
77
+ return `...(${acc} && { ${safeKey(key)}: validateRecord(${acc}, ${parserName}) })`;
78
+ };
79
+ /**
80
+ * Generates a spread entry for optional inline properties.
81
+ * The value expression is evaluated once and omitted when undefined.
82
+ */
83
+ const generateOptionalInlineProperty = (key, valueExpression) => {
84
+ return `...((value => value === undefined ? {} : { ${safeKey(key)}: value })(${valueExpression}))`;
85
+ };
86
+ /**
87
+ * Determines if a property schema should use ref imports for validation.
88
+ */
89
+ const shouldUseRefImport = (propSchema, useRefImports) => {
90
+ if (!useRefImports || !hasRef(propSchema)) {
91
+ return false;
92
+ }
93
+ const ref = propSchema.$ref;
94
+ // Skip external URI refs with property/definition fragments — these are not generated as files
95
+ const isUri = ref.startsWith('http://') || ref.startsWith('https://');
96
+ if (isUri && (ref.includes('#/properties/') || ref.includes('#/definitions/'))) {
97
+ return false;
98
+ }
99
+ return true;
100
+ };
101
+ /**
102
+ * Determines if an array property should use ref imports for its items.
103
+ */
104
+ const shouldUseArrayRefImport = (propSchema, useRefImports) => {
105
+ if (!useRefImports) {
106
+ return false;
107
+ }
108
+ if (!isSchemaObject(propSchema)) {
109
+ return false;
110
+ }
111
+ if (!('type' in propSchema) || propSchema.type !== 'array') {
112
+ return false;
113
+ }
114
+ return hasItems(propSchema) && hasRef(propSchema.items);
115
+ };
116
+ /**
117
+ * Determines if a property with additionalProperties should use ref imports.
118
+ */
119
+ const shouldUseRecordRefImport = (propSchema, useRefImports) => {
120
+ if (!useRefImports) {
121
+ return false;
122
+ }
123
+ if (!isSchemaObject(propSchema)) {
124
+ return false;
125
+ }
126
+ if (!('type' in propSchema) || propSchema.type !== 'object') {
127
+ return false;
128
+ }
129
+ if (!('additionalProperties' in propSchema)) {
130
+ return false;
131
+ }
132
+ const additionalProps = propSchema.additionalProperties;
133
+ return isSchemaObject(additionalProps) && hasRef(additionalProps);
134
+ };
135
+ /**
136
+ * Generates the value expression for a property based on its schema and options.
137
+ */
138
+ const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix) => {
139
+ // Handle direct $ref properties
140
+ if (shouldUseRefImport(propSchema, useRefImports)) {
141
+ const ref = propSchema.$ref;
142
+ return isRequired ? generateRequiredRefCall(key, ref, suffix) : generateOptionalRefCall(key, ref, suffix);
143
+ }
144
+ // Handle array properties with $ref items
145
+ if (shouldUseArrayRefImport(propSchema, useRefImports)) {
146
+ const items = propSchema.items;
147
+ const ref = items.$ref;
148
+ return isRequired ? generateRequiredArrayRefCall(key, ref, suffix) : generateOptionalArrayRefCall(key, ref, suffix);
149
+ }
150
+ // Handle object properties with additionalProperties $ref
151
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
152
+ const additionalProps = propSchema.additionalProperties;
153
+ const ref = additionalProps.$ref;
154
+ return isRequired
155
+ ? generateRequiredRecordRefCall(key, ref, suffix)
156
+ : generateOptionalRecordRefCall(key, ref, suffix);
157
+ }
158
+ // Handle non-schema object properties (true/false)
159
+ if (!isSchemaObject(propSchema)) {
160
+ return isRequired ? 'undefined' : generateOptionalInlineProperty(key, 'undefined');
161
+ }
162
+ // Generate standard validation expression
163
+ const defaultValue = getDefaultValue(propSchema);
164
+ const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired);
165
+ return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression);
166
+ };
167
+ /**
168
+ * Generates property entries for all properties in the schema.
169
+ */
170
+ const generatePropertyEntries = (schema, useRefImports, suffix) => {
171
+ if (!hasProperties(schema)) {
172
+ return [];
173
+ }
174
+ const entries = [];
175
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
176
+ const isRequired = isPropertyRequired(key, schema);
177
+ const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix);
178
+ // Determine if the property uses spread syntax (making it optional in the object literal)
179
+ const isOptional = value.startsWith('...');
180
+ entries.push({ key, value, isOptional });
181
+ }
182
+ return entries;
183
+ };
184
+ /**
185
+ * Generates a fallback value for a required property.
186
+ * This returns simple type-based defaults without pattern inference,
187
+ * matching TypeBox's coercion behavior.
188
+ */
189
+ const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
190
+ // Handle direct $ref properties - call the parser with undefined
191
+ if (useRefImports && hasRef(propSchema)) {
192
+ const ref = propSchema.$ref;
193
+ const parserName = generateParserName(refToName(ref, suffix));
194
+ return `${parserName}(undefined)`;
195
+ }
196
+ // Handle array properties with $ref items
197
+ if (useRefImports &&
198
+ isSchemaObject(propSchema) &&
199
+ propSchema.type === 'array' &&
200
+ hasItems(propSchema) &&
201
+ hasRef(propSchema.items)) {
202
+ return '[]';
203
+ }
204
+ // Handle object properties with additionalProperties $ref
205
+ if (useRefImports &&
206
+ isSchemaObject(propSchema) &&
207
+ propSchema.type === 'object' &&
208
+ 'additionalProperties' in propSchema) {
209
+ const additionalProps = propSchema.additionalProperties;
210
+ if (isSchemaObject(additionalProps) && hasRef(additionalProps)) {
211
+ return '{}';
212
+ }
213
+ }
214
+ // For non-schema objects, return undefined
215
+ if (!isSchemaObject(propSchema)) {
216
+ return 'undefined';
217
+ }
218
+ // Use explicit default if provided
219
+ if (hasDefault(propSchema)) {
220
+ return JSON.stringify(propSchema.default);
221
+ }
222
+ // Use first enum value if available
223
+ if (hasEnum(propSchema) && propSchema.enum.length > 0) {
224
+ return JSON.stringify(propSchema.enum[0]);
225
+ }
226
+ // Use first example if available
227
+ if (hasExamples(propSchema) && propSchema.examples.length > 0) {
228
+ return JSON.stringify(propSchema.examples[0]);
229
+ }
230
+ // Return simple type-based defaults without pattern inference
231
+ if (!hasType(propSchema)) {
232
+ return 'undefined';
233
+ }
234
+ switch (propSchema.type) {
235
+ case 'string':
236
+ return '""';
237
+ case 'number':
238
+ case 'integer':
239
+ return '0';
240
+ case 'boolean':
241
+ return 'false';
242
+ case 'array':
243
+ return '[]';
244
+ case 'object':
245
+ return '{}';
246
+ default:
247
+ return 'undefined';
248
+ }
249
+ };
250
+ /**
251
+ * Generates a fallback object with required properties filled with default values.
252
+ * This is used when input is not an object (undefined, null, etc.).
253
+ */
254
+ const generateFallbackObject = (schema, useRefImports, typeName, suffix) => {
255
+ if (!hasProperties(schema)) {
256
+ return `{} as ${typeName}`;
257
+ }
258
+ // For schemas with conditional branches (if/then/else), the merged properties
259
+ // may not fully represent all required fields. Use a simple cast to avoid
260
+ // generating incomplete fallback objects.
261
+ if (isSchemaObject(schema) && ('if' in schema || 'then' in schema || 'else' in schema)) {
262
+ return `{} as ${typeName}`;
263
+ }
264
+ const requiredProps = [];
265
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
266
+ const isRequired = isPropertyRequired(key, schema);
267
+ if (isRequired) {
268
+ const fallbackValue = generateFallbackValue(key, propSchema, useRefImports, suffix);
269
+ if (fallbackValue === 'undefined') {
270
+ return `{} as ${typeName}`;
271
+ }
272
+ requiredProps.push(` ${safeKey(key)}: ${fallbackValue},`);
273
+ }
274
+ }
275
+ if (requiredProps.length === 0) {
276
+ return `{} as ${typeName}`;
277
+ }
278
+ let result = '{\n';
279
+ for (let i = 0; i < requiredProps.length; i++) {
280
+ result += requiredProps[i];
281
+ if (i < requiredProps.length - 1) {
282
+ result += '\n';
283
+ }
284
+ }
285
+ result += '\n }';
286
+ return result;
287
+ };
288
+ /**
289
+ * Generates a parser for non-object schemas (string, number, boolean, array, etc.)
290
+ * that validates the input matches the expected primitive type before casting.
291
+ */
292
+ const generateNonObjectParser = (typeName, schema, strict) => {
293
+ const functionName = generateParserName(typeName);
294
+ if (!isSchemaObject(schema) || !hasType(schema)) {
295
+ // Schema without type information cannot be validated beyond a cast
296
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
297
+ }
298
+ if (strict) {
299
+ const assertion = generateScalarStrictAssertion(schema, typeName);
300
+ if (assertion === null) {
301
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
302
+ }
303
+ const returnExpr = schema.type === 'array' ? `[...(input as readonly unknown[])] as ${typeName}` : `input as ${typeName}`;
304
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n${assertion}\n return ${returnExpr};\n};`;
305
+ }
306
+ switch (schema.type) {
307
+ case 'string':
308
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`;
309
+ case 'number':
310
+ case 'integer':
311
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" ? input as ${typeName} : 0 as ${typeName};`;
312
+ case 'boolean':
313
+ return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
314
+ case 'array':
315
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`;
316
+ default:
317
+ return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
318
+ }
319
+ };
320
+ /**
321
+ * Generates a parser for empty object schemas or schemas with only additionalProperties.
322
+ * Validates the input is an object before casting, falling back to an empty object.
323
+ * Returns a shallow copy to avoid mutating the original input.
324
+ */
325
+ const generateEmptyObjectParser = (typeName, strict) => {
326
+ const functionName = generateParserName(typeName);
327
+ if (strict) {
328
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`;
329
+ }
330
+ return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
331
+ };
332
+ /**
333
+ * Converts a property key to a safe local variable name for caching.
334
+ * Replaces non-identifier characters with underscores.
335
+ */
336
+ const toVarName = (key) => {
337
+ const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_');
338
+ return `_${safe}`;
339
+ };
340
+ /**
341
+ * Returns the shape-predicate function name for a given type name.
342
+ * The predicate is generated alongside each parser and tells callers
343
+ * whether `input` is already in the shape produced by the parser's fast path.
344
+ */
345
+ const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
346
+ /**
347
+ * Generates a fast-path type check expression for a property.
348
+ * Returns null if the schema is too complex for a simple type check
349
+ * (e.g. has unions, enums that require more elaborate validation).
350
+ *
351
+ * When `useRefImports` is true, $ref properties (and arrays of $refs) are
352
+ * checked by calling the imported shape predicate, allowing parent parsers
353
+ * to fast-path through nested ref types without recursing into their parsers.
354
+ */
355
+ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
356
+ if (!isSchemaObject(schema))
357
+ return null;
358
+ // $ref via shape predicate (deep fast-path through nested types).
359
+ if (useRefImports && hasRef(schema)) {
360
+ const refName = refToName(schema.$ref, suffix);
361
+ return `${shapeValidatorName(refName)}(${varName})`;
362
+ }
363
+ // Skip fast path for complex schemas that need more elaborate validation
364
+ if (hasEnum(schema) ||
365
+ hasOneOf(schema) ||
366
+ hasAnyOf(schema) ||
367
+ hasAllOf(schema) ||
368
+ hasRef(schema) ||
369
+ 'not' in schema) {
370
+ return null;
371
+ }
372
+ if (!hasType(schema))
373
+ return null;
374
+ // Array of $refs: each item must satisfy the imported shape predicate.
375
+ if (useRefImports &&
376
+ schema.type === 'array' &&
377
+ hasItems(schema) &&
378
+ isSchemaObject(schema.items) &&
379
+ hasRef(schema.items)) {
380
+ const refName = refToName(schema.items.$ref, suffix);
381
+ return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
382
+ }
383
+ const checks = [];
384
+ switch (schema.type) {
385
+ case 'string': {
386
+ checks.push(`typeof ${varName} === "string"`);
387
+ if (hasPattern(schema))
388
+ checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
389
+ if (hasMinLength(schema))
390
+ checks.push(`${varName}.length >= ${schema.minLength}`);
391
+ if (hasMaxLength(schema))
392
+ checks.push(`${varName}.length <= ${schema.maxLength}`);
393
+ break;
394
+ }
395
+ case 'number':
396
+ case 'integer': {
397
+ checks.push(`typeof ${varName} === "number"`);
398
+ if (hasMinimum(schema))
399
+ checks.push(`${varName} >= ${schema.minimum}`);
400
+ if (hasMaximum(schema))
401
+ checks.push(`${varName} <= ${schema.maximum}`);
402
+ if (hasExclusiveMinimum(schema))
403
+ checks.push(`${varName} > ${schema.exclusiveMinimum}`);
404
+ if (hasExclusiveMaximum(schema))
405
+ checks.push(`${varName} < ${schema.exclusiveMaximum}`);
406
+ if (hasMultipleOf(schema))
407
+ checks.push(`${varName} % ${schema.multipleOf} === 0`);
408
+ break;
409
+ }
410
+ case 'boolean':
411
+ checks.push(`typeof ${varName} === "boolean"`);
412
+ break;
413
+ case 'array': {
414
+ checks.push(`Array.isArray(${varName})`);
415
+ if (hasMinItems(schema))
416
+ checks.push(`${varName}.length >= ${schema.minItems}`);
417
+ if (hasMaxItems(schema))
418
+ checks.push(`${varName}.length <= ${schema.maxItems}`);
419
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
420
+ checks.push(`new Set(${varName}).size === ${varName}.length`);
421
+ }
422
+ break;
423
+ }
424
+ case 'object':
425
+ checks.push(`isObject(${varName})`);
426
+ break;
427
+ default:
428
+ return null;
429
+ }
430
+ if (checks.length === 0)
431
+ return null;
432
+ // checks[0] is safe: we guard with checks.length === 0 above
433
+ let result = checks[0];
434
+ for (let i = 1; i < checks.length; i++) {
435
+ result += ' && ' + checks[i];
436
+ }
437
+ return result;
438
+ };
439
+ /**
440
+ * Determines if a property needs a local variable or can be inlined.
441
+ * Schema-object properties are always cached: the slow-path ternary chain
442
+ * reads each value 3-4x (typeof check, valid branch, undefined check, coerce),
443
+ * so a single hoisted load is strictly cheaper than the inlined optional chain.
444
+ */
445
+ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
446
+ // Non-schema-object properties (true/false JSON Schema literals) generate
447
+ // `undefined` with no value access, so caching would be wasted.
448
+ return isSchemaObject(propSchema);
449
+ };
450
+ /**
451
+ * Generates a parser for object schemas with properties.
452
+ *
453
+ * Uses an optimized function body with:
454
+ * - Early return for non-object input
455
+ * - Selective variable caching (only when needed)
456
+ * - Fast path that returns input directly when all properties are already valid
457
+ * - Slow path with optimized expressions
458
+ */
459
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict) => {
460
+ const functionName = generateParserName(typeName);
461
+ if (!hasProperties(schema)) {
462
+ if (strict) {
463
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return input as ${typeName};\n};`;
464
+ }
465
+ return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
466
+ }
467
+ const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix);
468
+ const properties = Object.entries(schema.properties);
469
+ const lines = [];
470
+ lines.push(`export const ${functionName} = (input: unknown): ${typeName} => {`);
471
+ if (strict) {
472
+ for (const assertionLine of generateObjectStrictAssertion(schema, typeName)) {
473
+ lines.push(assertionLine);
474
+ }
475
+ }
476
+ else {
477
+ lines.push(` if (!isObject(input)) return ${fallbackObject};`);
478
+ }
479
+ // First pass: determine if we can generate a fast path
480
+ const propInfo = [];
481
+ // Disable fast path when allOf $ref parsers need to be spread — the fast path
482
+ // returns input as-is and would skip those coercions.
483
+ const hasAllOfRefParsers = useRefImports &&
484
+ isSchemaObject(schema) &&
485
+ hasAllOf(schema) &&
486
+ schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
487
+ let canFastPath = !hasAllOfRefParsers;
488
+ const fastPathChecks = [];
489
+ for (const [key, propSchema] of properties) {
490
+ const isRequired = isPropertyRequired(key, schema);
491
+ const varName = toVarName(key);
492
+ propInfo.push({ key, varName, isRequired, propSchema });
493
+ // Check if this property can participate in fast path
494
+ if (canFastPath) {
495
+ // Records of refs require iterating every value to check shape — too
496
+ // expensive to inline into the parser's fast path. Disable.
497
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
498
+ canFastPath = false;
499
+ }
500
+ else {
501
+ const check = generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
502
+ if (check === null) {
503
+ canFastPath = false;
504
+ }
505
+ else {
506
+ if (isRequired) {
507
+ fastPathChecks.push(check);
508
+ }
509
+ else {
510
+ fastPathChecks.push(`(${varName} === undefined || ${check})`);
511
+ }
512
+ }
513
+ }
514
+ }
515
+ }
516
+ // Second pass: generate variable declarations only for properties that need them
517
+ for (const { key, varName, propSchema } of propInfo) {
518
+ if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
519
+ lines.push(` const ${varName} = ${safeAccessor('input', key)};`);
520
+ }
521
+ }
522
+ // Emit a warning for any input key not declared in the schema's properties
523
+ if (logWarnings && propInfo.length > 0) {
524
+ const keysList = propInfo.map(({ key }) => JSON.stringify(key)).join(', ');
525
+ lines.push(` const _knownKeys = new Set([${keysList}]);`);
526
+ lines.push(` for (const _k in input) {`);
527
+ lines.push(` if (!_knownKeys.has(_k)) {`);
528
+ lines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`);
529
+ lines.push(` }`);
530
+ lines.push(` }`);
531
+ }
532
+ // Generate fast-path check if possible
533
+ if (canFastPath && fastPathChecks.length > 0) {
534
+ let fastPathExpr = fastPathChecks[0];
535
+ for (let i = 1; i < fastPathChecks.length; i++) {
536
+ fastPathExpr += ' && ' + fastPathChecks[i];
537
+ }
538
+ lines.push(` if (${fastPathExpr}) return { ...input } as ${typeName};`);
539
+ }
540
+ // Generate slow-path object construction
541
+ const objectLines = [];
542
+ objectLines.push(' ...input,');
543
+ // Spread allOf $ref parsers so their field coercions are applied before
544
+ // the explicit property validations below.
545
+ if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
546
+ for (const entry of schema.allOf) {
547
+ if (isSchemaObject(entry) && hasRef(entry)) {
548
+ const parserName = generateParserName(refToName(entry.$ref, suffix));
549
+ objectLines.push(` ...(${parserName}(input) as Record<string, unknown>),`);
550
+ }
551
+ }
552
+ }
553
+ for (const { key, varName, isRequired, propSchema } of propInfo) {
554
+ const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
555
+ const accessor = shouldCache ? varName : safeAccessor('input', key);
556
+ // Handle direct $ref properties via imported parsers
557
+ if (shouldUseRefImport(propSchema, useRefImports)) {
558
+ const ref = propSchema.$ref;
559
+ const parserName = generateParserName(refToName(ref, suffix));
560
+ if (isRequired) {
561
+ objectLines.push(` ${safeKey(key)}: ${parserName}(${accessor}),`);
562
+ }
563
+ else {
564
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${parserName}(${accessor}) }),`);
565
+ }
566
+ continue;
567
+ }
568
+ // Handle array properties with $ref items
569
+ if (shouldUseArrayRefImport(propSchema, useRefImports)) {
570
+ const items = propSchema.items;
571
+ const ref = items.$ref;
572
+ const parserName = generateParserName(refToName(ref, suffix));
573
+ if (isRequired) {
574
+ objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${parserName}),`);
575
+ }
576
+ else {
577
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
578
+ }
579
+ continue;
580
+ }
581
+ // Handle object properties with additionalProperties $ref
582
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
583
+ const additionalProps = propSchema.additionalProperties;
584
+ const ref = additionalProps.$ref;
585
+ const parserName = generateParserName(refToName(ref, suffix));
586
+ if (isRequired) {
587
+ objectLines.push(` ${safeKey(key)}: validateRecord(${accessor}, ${parserName}),`);
588
+ }
589
+ else {
590
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
591
+ }
592
+ continue;
593
+ }
594
+ // Handle non-schema-object properties
595
+ if (!isSchemaObject(propSchema)) {
596
+ if (isRequired) {
597
+ objectLines.push(` ${safeKey(key)}: undefined,`);
598
+ }
599
+ continue;
600
+ }
601
+ // Generate validation expression
602
+ const defaultValue = getDefaultValue(propSchema);
603
+ // For optional properties in the slow path, we know the value is not undefined
604
+ // because we're inside the `...(accessor !== undefined && { ... })` check
605
+ const knownNotUndefined = !isRequired;
606
+ const valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined);
607
+ if (isRequired) {
608
+ objectLines.push(` ${safeKey(key)}: ${valueExpr},`);
609
+ }
610
+ else {
611
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${valueExpr} }),`);
612
+ }
613
+ }
614
+ lines.push(` return {`);
615
+ // objectLines[0] is safe: objectLines is always non-empty (has at least the closing brace line)
616
+ let objectBody = objectLines[0];
617
+ for (let i = 1; i < objectLines.length; i++) {
618
+ objectBody += '\n' + objectLines[i];
619
+ }
620
+ lines.push(objectBody);
621
+ lines.push(` } as unknown as ${typeName};`);
622
+ lines.push(`}`);
623
+ // lines[0] is safe: lines is always non-empty (it always has the function declaration)
624
+ let result = lines[0];
625
+ for (let i = 1; i < lines.length; i++) {
626
+ result += '\n' + lines[i];
627
+ }
628
+ return result;
629
+ };
630
+ /**
631
+ * Generates a parser for schemas that have both properties AND patternProperties.
632
+ * Parses the known properties first, then iterates remaining keys to match patterns.
633
+ */
634
+ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict) => {
635
+ const functionName = generateParserName(typeName);
636
+ const entries = generatePropertyEntries(schema, useRefImports, suffix);
637
+ // Build the known property lines for the initial object
638
+ const propertyLines = entries.map((entry) => {
639
+ if (entry.isOptional) {
640
+ return ` ${entry.value},`;
641
+ }
642
+ return ` ${safeKey(entry.key)}: ${entry.value},`;
643
+ });
644
+ // Find the first pattern with a $ref for parser delegation
645
+ if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
646
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
647
+ }
648
+ const patternProps = schema.patternProperties;
649
+ const patterns = Object.entries(patternProps);
650
+ const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
651
+ if (!refPattern || !useRefImports) {
652
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
653
+ }
654
+ const [pattern, patternSchema] = refPattern;
655
+ const ref = patternSchema.$ref;
656
+ const parserName = generateParserName(refToName(ref, suffix));
657
+ const assignmentCode = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
658
+ const escapedPattern = escapeRegexPattern(pattern);
659
+ const inputSpread = ' ...input,';
660
+ let objectProperties = inputSpread;
661
+ if (propertyLines.length > 0) {
662
+ for (const line of propertyLines) {
663
+ objectProperties += '\n' + line;
664
+ }
665
+ }
666
+ const notObjectBranch = strict
667
+ ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
668
+ : ` return {} as unknown as ${typeName};`;
669
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
670
+ if (!isObject(input)) {
671
+ ${notObjectBranch}
672
+ }
673
+ const result = {
674
+ ${objectProperties}
675
+ } as unknown as ${typeName};
676
+ for (const key in input) {
677
+ if (/${escapedPattern}/.test(key)) {
678
+ const value = input[key];
679
+ ${assignmentCode}
680
+ }
681
+ }
682
+ return result;
683
+ };`;
684
+ };
685
+ /**
686
+ * Generates a parser for schemas with additionalProperties.
687
+ */
688
+ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
689
+ const functionName = generateParserName(typeName);
690
+ const additionalProps = schema.additionalProperties;
691
+ // Check if additionalProps is defined before using it
692
+ if (!additionalProps) {
693
+ return generateEmptyObjectParser(typeName, strict);
694
+ }
695
+ // If additionalProperties is a $ref and useRefImports is true, generate a loop
696
+ if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
697
+ const ref = additionalProps.$ref;
698
+ const parserName = generateParserName(refToName(ref, suffix));
699
+ if (strict) {
700
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${parserName}) as ${typeName};\n};`;
701
+ }
702
+ return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
703
+ }
704
+ // Handle additionalProperties with a known type by generating inline validation
705
+ if (isSchemaObject(additionalProps) && hasType(additionalProps)) {
706
+ const inlineParser = generateInlineValueParser(additionalProps);
707
+ if (inlineParser) {
708
+ if (strict) {
709
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`;
710
+ }
711
+ return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
712
+ }
713
+ }
714
+ // Otherwise, just validate the input type and shallow copy
715
+ if (strict) {
716
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`;
717
+ }
718
+ return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
719
+ };
720
+ /**
721
+ * Generates an inline arrow function that validates a single value based on
722
+ * the additionalProperties schema type. Returns null if the type is not supported.
723
+ *
724
+ * Used to generate parsers for Record-like types where values have a known
725
+ * primitive or array type (e.g. Record<string, string> or Record<string, string[]>).
726
+ */
727
+ const generateInlineValueParser = (schema) => {
728
+ if (!isSchemaObject(schema) || !hasType(schema)) {
729
+ return null;
730
+ }
731
+ switch (schema.type) {
732
+ case 'string':
733
+ return '(value: unknown) => typeof value === "string" ? value : ""';
734
+ case 'number':
735
+ case 'integer':
736
+ return '(value: unknown) => typeof value === "number" ? value : 0';
737
+ case 'boolean':
738
+ return '(value: unknown) => typeof value === "boolean" ? value : false';
739
+ case 'array':
740
+ return '(value: unknown) => Array.isArray(value) ? value : []';
741
+ default:
742
+ return null;
743
+ }
744
+ };
745
+ /**
746
+ * Escapes special regex characters in a pattern string for use in a regex literal.
747
+ * This escapes backslashes and forward slashes for safe inclusion in /pattern/ syntax.
748
+ */
749
+ const escapeRegexPattern = (pattern) => {
750
+ // Escape backslashes first, then forward slashes
751
+ return pattern.replace(/\\/g, '\\\\').replace(/\//g, '\\/');
752
+ };
753
+ /**
754
+ * Generates a parser for schemas with patternProperties.
755
+ * Handles both patternProperties and specification-extensions (x- prefix).
756
+ */
757
+ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
758
+ const functionName = generateParserName(typeName);
759
+ if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
760
+ return generateEmptyObjectParser(typeName, strict);
761
+ }
762
+ const patternProps = schema.patternProperties;
763
+ // Find the first pattern and its schema
764
+ const patterns = Object.entries(patternProps);
765
+ if (patterns.length === 0) {
766
+ return generateEmptyObjectParser(typeName, strict);
767
+ }
768
+ const [pattern, patternSchema] = patterns[0];
769
+ let patternAssignment = '(result as Record<string, unknown>)[key] = value;';
770
+ // Use imported parser when pattern schema points to a $ref.
771
+ if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
772
+ const ref = patternSchema.$ref;
773
+ const parserName = generateParserName(refToName(ref, suffix));
774
+ patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
775
+ }
776
+ else if (patternSchema === false) {
777
+ // `false` means no values are allowed for matching keys.
778
+ patternAssignment = '';
779
+ }
780
+ // Escape the pattern for safe inclusion in generated code
781
+ const escapedPattern = escapeRegexPattern(pattern);
782
+ const notObjectBranch = strict
783
+ ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
784
+ : ` return {} as unknown as ${typeName};`;
785
+ // Generate a parser that handles both pattern matching and x- extensions
786
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
787
+ if (!isObject(input)) {
788
+ ${notObjectBranch}
789
+ }
790
+ const result = {
791
+ ...input,
792
+ } as unknown as ${typeName};
793
+ for (const key in input) {
794
+ if (/${escapedPattern}/.test(key)) {
795
+ const value = input[key];
796
+ ${patternAssignment}
797
+ }
798
+ }
799
+ return result;
800
+ };`;
801
+ };
802
+ /**
803
+ * Generates a parser for conditional schemas with if/then/else.
804
+ * This always generates parser calls regardless of useRefImports setting,
805
+ * because conditional logic requires delegating to the appropriate parser.
806
+ */
807
+ const generateConditionalParser = (schema, typeName, suffix) => {
808
+ const functionName = generateParserName(typeName);
809
+ // Extract the condition and branches
810
+ const ifSchema = schema.if;
811
+ const thenSchema = schema.then;
812
+ const elseSchema = schema.else;
813
+ // Check if branches are defined and have $ref
814
+ const thenHasRef = thenSchema && isSchemaObject(thenSchema) && hasRef(thenSchema);
815
+ const elseHasRef = elseSchema && isSchemaObject(elseSchema) && hasRef(elseSchema);
816
+ if (!thenHasRef || !elseHasRef) {
817
+ // Non-$ref branches (e.g. if/then/else providing defaults for properties).
818
+ // Flatten all three branches into a single object schema and generate a
819
+ // regular object parser from the merged property set.
820
+ const mergedSchema = getConditionalObjectSchema(schema);
821
+ if (mergedSchema) {
822
+ return generateObjectParser(mergedSchema, typeName, false, suffix);
823
+ }
824
+ return generateEmptyObjectParser(typeName);
825
+ }
826
+ // Check if the condition is checking for $ref property
827
+ const isRefCondition = ifSchema &&
828
+ isSchemaObject(ifSchema) &&
829
+ 'required' in ifSchema &&
830
+ Array.isArray(ifSchema.required) &&
831
+ ifSchema.required.includes('$ref');
832
+ if (!isRefCondition) {
833
+ // Condition is not a $ref check — flatten into an object parser
834
+ const mergedSchema = getConditionalObjectSchema(schema);
835
+ if (mergedSchema) {
836
+ return generateObjectParser(mergedSchema, typeName, false, suffix);
837
+ }
838
+ return generateEmptyObjectParser(typeName);
839
+ }
840
+ const thenRef = thenSchema.$ref;
841
+ const elseRef = elseSchema.$ref;
842
+ const thenParserName = generateParserName(refToName(thenRef, suffix));
843
+ const elseParserName = generateParserName(refToName(elseRef, suffix));
844
+ const thenTypeName = refToName(thenRef, suffix);
845
+ return `export const ${functionName} = (input: unknown): ${typeName} | ${thenTypeName} =>
846
+ hasRef(input) ? ${thenParserName}(input) : ${elseParserName}(input)
847
+ `;
848
+ };
849
+ /**
850
+ * Builds an object schema from conditional if/then keywords when the schema does not
851
+ * declare type: "object". Flattens if/then/else property sets into a single object schema.
852
+ */
853
+ const getConditionalObjectSchema = (schema) => {
854
+ if (!isSchemaObject(schema)) {
855
+ return null;
856
+ }
857
+ if (!('if' in schema) || !('then' in schema)) {
858
+ return null;
859
+ }
860
+ const ifSchema = schema.if;
861
+ const thenSchema = schema.then;
862
+ const elseSchema = 'else' in schema ? schema.else : undefined;
863
+ if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) {
864
+ return null;
865
+ }
866
+ const ifProperties = ifSchema.properties;
867
+ const thenProperties = thenSchema.properties;
868
+ const elseProperties = elseSchema && isSchemaObject(elseSchema) ? elseSchema.properties : undefined;
869
+ const hasIfProperties = ifProperties && typeof ifProperties === 'object';
870
+ const hasThenProperties = thenProperties && typeof thenProperties === 'object';
871
+ const hasElseProperties = elseProperties && typeof elseProperties === 'object';
872
+ if (!hasIfProperties && !hasThenProperties && !hasElseProperties) {
873
+ return null;
874
+ }
875
+ const required = new Set();
876
+ if (Array.isArray(ifSchema.required)) {
877
+ for (const key of ifSchema.required) {
878
+ required.add(key);
879
+ }
880
+ }
881
+ // If the `if` condition checks for a `const` value on a property, that property
882
+ // is effectively required (the schema only applies when the condition is true).
883
+ if (hasIfProperties && ifProperties && typeof ifProperties === 'object') {
884
+ for (const [key, propSchema] of Object.entries(ifProperties)) {
885
+ if (isSchemaObject(propSchema) && hasConst(propSchema)) {
886
+ required.add(key);
887
+ }
888
+ }
889
+ }
890
+ if (Array.isArray(thenSchema.required)) {
891
+ for (const key of thenSchema.required) {
892
+ required.add(key);
893
+ }
894
+ }
895
+ return {
896
+ type: 'object',
897
+ properties: {
898
+ // else properties first so then properties take precedence on overlap
899
+ ...(hasElseProperties ? elseProperties : {}),
900
+ ...(hasIfProperties ? ifProperties : {}),
901
+ ...(hasThenProperties ? thenProperties : {}),
902
+ },
903
+ ...(required.size > 0 ? { required: Array.from(required) } : {}),
904
+ };
905
+ };
906
+ /**
907
+ * Generates a parser for SchemaObject that validates all JSON Schema 2020-12 properties.
908
+ * This handles the special case where a schema can be any valid JSON Schema.
909
+ */
910
+ const generateSchemaObjectParser = (typeName) => {
911
+ const functionName = generateParserName(typeName);
912
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
913
+ if (typeof input === 'boolean') {
914
+ return input as ${typeName};
915
+ }
916
+
917
+ if (!isObject(input)) {
918
+ return {} as ${typeName};
919
+ }
920
+
921
+ return input as ${typeName};
922
+ };`;
923
+ };
924
+ /**
925
+ * Determines the appropriate parser generation strategy for a schema.
926
+ */
927
+ const selectParserStrategy = (schema, typeName, options) => {
928
+ const useRefImports = options?.useRefImports ?? false;
929
+ const logWarnings = options?.logWarnings ?? false;
930
+ const strict = options?.strict ?? false;
931
+ const suffix = options?.typeSuffix ?? '';
932
+ // Special case for the self-referential JSON Schema meta-schema type (e.g.
933
+ // `Schema` / `SchemaObject`) - it can be any JSON Schema.
934
+ if (typeName === `Schema${suffix}`) {
935
+ return generateSchemaObjectParser(typeName);
936
+ }
937
+ const isObjectLikeSchema = isObjectSchema(schema) ||
938
+ (isSchemaObject(schema) && ('patternProperties' in schema || 'additionalProperties' in schema));
939
+ // Handle non-object schemas with type-appropriate validation
940
+ if (!isObjectLikeSchema && !isSchemaObject(schema)) {
941
+ return generateNonObjectParser(typeName, schema, strict);
942
+ }
943
+ // Handle schemas with both properties AND patternProperties.
944
+ // This generates a parser that handles known properties and also iterates
945
+ // pattern-matched keys (e.g. responses with "default" + "200", "4XX").
946
+ if (hasProperties(schema) && isSchemaObject(schema) && 'patternProperties' in schema) {
947
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
948
+ }
949
+ // Handle schemas that have explicit properties — generate a full object parser.
950
+ // This intentionally runs before if/then checks so that schemas with both
951
+ // properties AND conditional keywords use all declared properties rather than
952
+ // only the if/then fragment.
953
+ if (hasProperties(schema)) {
954
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
955
+ }
956
+ // Handle conditional schemas (if/then/else) for schemas without explicit properties.
957
+ if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
958
+ return generateConditionalParser(schema, typeName, suffix);
959
+ }
960
+ // Handle conditional schemas that only define if/then object fragments.
961
+ // We flatten the fragments into a regular object parser.
962
+ const conditionalObjectSchema = getConditionalObjectSchema(schema);
963
+ if (conditionalObjectSchema) {
964
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict);
965
+ }
966
+ // Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
967
+ if (!isObjectLikeSchema) {
968
+ return generateNonObjectParser(typeName, schema, strict);
969
+ }
970
+ // Handle schemas with patternProperties (but no properties)
971
+ if ('patternProperties' in schema) {
972
+ return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict);
973
+ }
974
+ // Handle schemas with additionalProperties as true or false (but no properties)
975
+ if ('additionalProperties' in schema && !hasProperties(schema)) {
976
+ const additionalProps = schema.additionalProperties;
977
+ // If additionalProperties is true or false, validate it is an object
978
+ if (additionalProps === true || additionalProps === false) {
979
+ return generateEmptyObjectParser(typeName, strict);
980
+ }
981
+ // Otherwise, handle as a schema (could be a $ref or object schema)
982
+ return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
983
+ }
984
+ // Handle empty object schemas
985
+ if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
986
+ return generateEmptyObjectParser(typeName, strict);
987
+ }
988
+ // Default fallback - validate it is an object since we passed the isObjectSchema check
989
+ return generateEmptyObjectParser(typeName, strict);
990
+ };
991
+ /**
992
+ * Generates a safe parser as an arrow function that never throws.
993
+ * The parser provides default values for all missing or invalid fields.
994
+ *
995
+ * When useRefImports is enabled, properties with $ref will call the imported
996
+ * parser function (e.g., parseContact(input?.contact)) instead of inlining
997
+ * the resolved schema's validation logic. Array properties whose items are a
998
+ * $ref will map each element through the imported parser.
999
+ */
1000
+ export const generateParserFunction = (schema, typeName, options) => {
1001
+ return selectParserStrategy(schema, typeName, options);
1002
+ };
1003
+ /**
1004
+ * Returns the predicate function name for a generated shape validator.
1005
+ * @example shapeValidatorFunctionName('CustomerObject') // 'validateCustomerObjectShape'
1006
+ */
1007
+ export const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
1008
+ /**
1009
+ * Generates a `validate{TypeName}Shape(input)` predicate that returns true
1010
+ * iff `input` already matches the shape produced by the parser's fast path —
1011
+ * i.e. the parser would return `{ ...input } as TypeName` without coercion.
1012
+ *
1013
+ * Parents call this predicate to fast-path through nested ref properties
1014
+ * (and arrays of refs) without recursing into their parser.
1015
+ *
1016
+ * Returns a stub that always returns `false` for schemas that cannot be
1017
+ * predicated (composition, conditionals, pattern properties, complex refs
1018
+ * in additionalProperties). The stub is safe: parents calling it will fall
1019
+ * through to the slow path, matching the pre-deep-fast-path behavior.
1020
+ */
1021
+ export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '') => {
1022
+ const fnName = shapeValidatorName(typeName);
1023
+ const stub = `export const ${fnName} = (_input: unknown): boolean => false;`;
1024
+ if (!isSchemaObject(schema))
1025
+ return stub;
1026
+ if (!hasProperties(schema))
1027
+ return stub;
1028
+ // Composition / conditional schemas can match in many shapes — bail.
1029
+ if (hasOneOf(schema) ||
1030
+ hasAnyOf(schema) ||
1031
+ hasAllOf(schema) ||
1032
+ 'not' in schema ||
1033
+ 'patternProperties' in schema ||
1034
+ 'if' in schema ||
1035
+ 'then' in schema ||
1036
+ 'else' in schema) {
1037
+ return stub;
1038
+ }
1039
+ const properties = Object.entries(schema.properties);
1040
+ const checks = [];
1041
+ for (const [key, propSchema] of properties) {
1042
+ // Records of refs require iterating every value — too expensive for the fast path.
1043
+ if (shouldUseRecordRefImport(propSchema, useRefImports)) {
1044
+ return stub;
1045
+ }
1046
+ const accessor = safeAccessor('input', key);
1047
+ const isRequired = isPropertyRequired(key, schema);
1048
+ const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
1049
+ if (check === null) {
1050
+ return stub;
1051
+ }
1052
+ if (isRequired) {
1053
+ checks.push(check);
1054
+ }
1055
+ else {
1056
+ checks.push(`(${accessor} === undefined || ${check})`);
1057
+ }
1058
+ }
1059
+ if (checks.length === 0) {
1060
+ return `export const ${fnName} = (input: unknown): boolean => isObject(input);`;
1061
+ }
1062
+ let body = checks[0];
1063
+ for (let i = 1; i < checks.length; i++) {
1064
+ body += '\n && ' + checks[i];
1065
+ }
1066
+ return `export const ${fnName} = (input: unknown): boolean => {
1067
+ if (!isObject(input)) return false;
1068
+ return ${body};
1069
+ };`;
1070
+ };