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