@amritk/generate-validators 0.3.1 → 0.4.1

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.
@@ -27,4 +27,4 @@ export type GeneratedFile = {
27
27
  * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
28
28
  * ```
29
29
  */
30
- export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string) => Promise<GeneratedFile[]>;
30
+ export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
@@ -13,6 +13,11 @@ type CollectValidatorImportsOptions = {
13
13
  * are excluded from the import list (they were never generated as files).
14
14
  */
15
15
  readonly rootSchema?: Record<string, unknown> | undefined;
16
+ /**
17
+ * Suffix appended to every type/validator name derived from a `$ref`. Must
18
+ * match the suffix used when generating the referenced files. Defaults to `''`.
19
+ */
20
+ readonly typeSuffix?: string;
16
21
  };
17
22
  /**
18
23
  * Collects import statements for all $ref dependencies of a schema.
@@ -22,7 +27,7 @@ type CollectValidatorImportsOptions = {
22
27
  * ```typescript
23
28
  * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
24
29
  * collectValidatorImports(schema)
25
- * // ["import { type ContactObject, validateContactObject } from './contact-object'"]
30
+ * // ["import { type Contact, validateContact } from './contact'"]
26
31
  * ```
27
32
  */
28
33
  export declare const collectValidatorImports: (schema: JSONSchema, options?: CollectValidatorImportsOptions) => string[];
@@ -12,6 +12,11 @@ type GenerateValidatorFileOptions = {
12
12
  * The root schema document. Used to filter out unresolvable refs.
13
13
  */
14
14
  readonly rootSchema?: Record<string, unknown>;
15
+ /**
16
+ * Suffix appended to every type/validator name derived from a `$ref`.
17
+ * Defaults to `''` (no suffix).
18
+ */
19
+ readonly typeSuffix?: string;
15
20
  };
16
21
  /**
17
22
  * Generates a complete TypeScript validator file from a JSON Schema.
@@ -21,4 +21,4 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
21
21
  * // }
22
22
  * ```
23
23
  */
24
- export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string) => string;
24
+ export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string, suffix?: string) => string;
package/dist/index.js CHANGED
@@ -132,15 +132,15 @@ var refToFilename2 = (ref) => {
132
132
  }
133
133
  return filename;
134
134
  };
135
- var kebabToPascal = (kebab) => {
135
+ var kebabToPascal = (kebab, suffix) => {
136
136
  const words = kebab.split("-");
137
137
  let pascalCase = "";
138
138
  for (const word of words) {
139
139
  pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
140
140
  }
141
- return pascalCase + "Object";
141
+ return pascalCase + suffix;
142
142
  };
143
- var refToName = (ref) => kebabToPascal(refToFilename2(ref));
143
+ var refToName = (ref, suffix = "") => kebabToPascal(refToFilename2(ref), suffix);
144
144
 
145
145
  // ../helpers/dist/resolve-dynamic-refs.js
146
146
  var resolveDynamicRefs = (schema, dynamicRefMap) => {
@@ -415,15 +415,15 @@ var refToFilename4 = (ref) => {
415
415
  }
416
416
  return filename;
417
417
  };
418
- var kebabToPascal2 = (kebab) => {
418
+ var kebabToPascal2 = (kebab, suffix) => {
419
419
  const words = kebab.split("-");
420
420
  let pascalCase = "";
421
421
  for (const word of words) {
422
422
  pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
423
423
  }
424
- return pascalCase + "Object";
424
+ return pascalCase + suffix;
425
425
  };
426
- var refToName2 = (ref) => kebabToPascal2(refToFilename4(ref));
426
+ var refToName2 = (ref, suffix = "") => kebabToPascal2(refToFilename4(ref), suffix);
427
427
  var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
428
428
  var safeKey = (key) => {
429
429
  if (JS_IDENTIFIER.test(key)) {
@@ -541,13 +541,13 @@ var getUnbrandedType = (schema, options = {}) => {
541
541
  if (!schema.$ref.startsWith("#")) {
542
542
  return "unknown";
543
543
  }
544
- return refToName2(schema.$ref);
544
+ return refToName2(schema.$ref, options.typeSuffix);
545
545
  }
546
546
  if (schema.$dynamicRef) {
547
547
  if (schema.$dynamicRef === "#meta") {
548
- return "Schema";
548
+ return `Schema${options.typeSuffix ?? ""}`;
549
549
  }
550
- return refToName2(schema.$dynamicRef);
550
+ return refToName2(schema.$dynamicRef, options.typeSuffix);
551
551
  }
552
552
  if (schema.const !== undefined) {
553
553
  return JSON.stringify(schema.const);
@@ -594,7 +594,7 @@ var getUnbrandedType = (schema, options = {}) => {
594
594
  if (conditionalResult) {
595
595
  const baseType = getTypeScriptType(conditionalResult.schema, options);
596
596
  if (conditionalResult.thenRef) {
597
- return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
597
+ return `(${baseType}) & ${refToName2(conditionalResult.thenRef, options.typeSuffix)}`;
598
598
  }
599
599
  return baseType;
600
600
  }
@@ -675,6 +675,31 @@ var getUnbrandedType = (schema, options = {}) => {
675
675
  case "object":
676
676
  if (schema.properties) {
677
677
  const readonlyPrefix = options.readonly ? "readonly " : "";
678
+ const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject2(p) && (typeof p.description === "string" || typeof p.$comment === "string"));
679
+ if (hasDescriptions) {
680
+ let properties2 = "";
681
+ let first2 = true;
682
+ for (const key in schema.properties) {
683
+ const propSchema = schema.properties[key];
684
+ const isRequired = schema.required?.includes(key) ?? false;
685
+ const optional = isRequired ? "" : "?";
686
+ const propType = getTypeScriptType(propSchema, options);
687
+ const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined;
688
+ if (!first2)
689
+ properties2 += `
690
+ `;
691
+ first2 = false;
692
+ if (inlineDescription) {
693
+ properties2 += " /** " + inlineDescription + ` */
694
+ ` + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
695
+ } else {
696
+ properties2 += " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
697
+ }
698
+ }
699
+ return `{
700
+ ` + properties2 + `
701
+ }`;
702
+ }
678
703
  let properties = "";
679
704
  let first = true;
680
705
  for (const key in schema.properties) {
@@ -716,8 +741,9 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
716
741
  if (!isObjectLikeSchema(schema)) {
717
742
  const tsType = getTypeScriptType(schema, options);
718
743
  let result = "";
719
- if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
720
- result += buildJsDocBlock(typeName, schema.$comment);
744
+ const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
745
+ if (topLevelComment) {
746
+ result += buildJsDocBlock(typeName, topLevelComment);
721
747
  }
722
748
  result += `export type ${typeName} = ${tsType};`;
723
749
  return result;
@@ -728,9 +754,10 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
728
754
  const conditionalThenRef = conditionalResult?.thenRef ?? null;
729
755
  let jsDocTitle;
730
756
  let jsDocDescription;
731
- if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
757
+ const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
758
+ if (topLevelComment) {
732
759
  jsDocTitle = typeName;
733
- jsDocDescription = schema.$comment;
760
+ jsDocDescription = topLevelComment;
734
761
  }
735
762
  const hasProperties2 = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0;
736
763
  const hasAdditionalProperties2 = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === "object";
@@ -787,12 +814,12 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
787
814
  if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) {
788
815
  for (const entry of schema.allOf) {
789
816
  if (isSchemaObject2(entry) && entry.$ref) {
790
- allOfIntersections.push(refToName2(entry.$ref));
817
+ allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix));
791
818
  }
792
819
  }
793
820
  }
794
821
  if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) {
795
- allOfIntersections.push(refToName2(schema.$ref));
822
+ allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix));
796
823
  }
797
824
  let result = "";
798
825
  if (jsDocTitle && jsDocDescription) {
@@ -802,7 +829,7 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
802
829
  ` + properties + `
803
830
  }`;
804
831
  if (conditionalThenRef) {
805
- typeBody += " & " + refToName2(conditionalThenRef);
832
+ typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix);
806
833
  }
807
834
  for (const intersectionType of allOfIntersections) {
808
835
  typeBody += " & " + intersectionType;
@@ -876,9 +903,9 @@ var hasMultipleOf = (schema) => {
876
903
  };
877
904
 
878
905
  // src/generators/collect-validator-imports.ts
879
- var buildImport = (ref) => {
906
+ var buildImport = (ref, suffix) => {
880
907
  const filename = refToFilename(ref);
881
- const typeName = refToName(ref);
908
+ const typeName = refToName(ref, suffix);
882
909
  const validatorName = `validate${typeName}`;
883
910
  return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
884
911
  };
@@ -923,6 +950,7 @@ var collectDirectRefs = (schema) => {
923
950
  var collectValidatorImports = (schema, options) => {
924
951
  const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
925
952
  const rootSchema = options?.rootSchema;
953
+ const typeSuffix = options?.typeSuffix ?? "";
926
954
  const refs = collectDirectRefs(schema);
927
955
  const seen = new Set;
928
956
  const imports = [];
@@ -939,7 +967,7 @@ var collectValidatorImports = (schema, options) => {
939
967
  }
940
968
  seen.add(filename);
941
969
  const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
942
- imports.push(buildImport(importRef));
970
+ imports.push(buildImport(importRef, typeSuffix));
943
971
  }
944
972
  return imports;
945
973
  };
@@ -993,7 +1021,7 @@ var wrongTypeCondition = (accessor, type) => {
993
1021
  return "";
994
1022
  }
995
1023
  };
996
- var generatePropertyChecks = (key, propSchema, isRequired) => {
1024
+ var generatePropertyChecks = (key, propSchema, isRequired, suffix) => {
997
1025
  if (!isSchemaObject3(propSchema))
998
1026
  return [];
999
1027
  const raw = `obj[${JSON.stringify(key)}]`;
@@ -1001,7 +1029,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
1001
1029
  const lines = [];
1002
1030
  if (hasRef(propSchema)) {
1003
1031
  const ref = propSchema.$ref;
1004
- const vName = validatorName(refToName(ref));
1032
+ const vName = validatorName(refToName(ref, suffix));
1005
1033
  if (isRequired) {
1006
1034
  lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
1007
1035
  lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
@@ -1127,7 +1155,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
1127
1155
  if (t === "array" && hasItems(propSchema)) {
1128
1156
  const itemSchema = propSchema.items;
1129
1157
  if (hasRef(itemSchema)) {
1130
- const vName = validatorName(refToName(itemSchema.$ref));
1158
+ const vName = validatorName(refToName(itemSchema.$ref, suffix));
1131
1159
  lines.push(` if (Array.isArray(${raw})) {`);
1132
1160
  lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
1133
1161
  lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`);
@@ -1151,19 +1179,19 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
1151
1179
  }
1152
1180
  return lines;
1153
1181
  };
1154
- var generateObjectValidator = (schema, typeName) => {
1182
+ var generateObjectValidator = (schema, typeName, suffix) => {
1155
1183
  const vName = validatorName(typeName);
1156
1184
  const required = new Set(hasRequired(schema) ? schema.required : []);
1157
1185
  const properties = hasProperties(schema) ? schema.properties : {};
1158
1186
  const propertyLines = [];
1159
1187
  for (const [key, propSchema] of Object.entries(properties)) {
1160
- const checks = generatePropertyChecks(key, propSchema, required.has(key));
1188
+ const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix);
1161
1189
  if (checks.length > 0) {
1162
1190
  propertyLines.push(...checks);
1163
1191
  }
1164
1192
  }
1165
1193
  if (hasAdditionalProperties(schema) && isSchemaObject3(schema.additionalProperties) && hasRef(schema.additionalProperties)) {
1166
- const vRefName = validatorName(refToName(schema.additionalProperties.$ref));
1194
+ const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix));
1167
1195
  propertyLines.push(` for (const _key of Object.keys(obj)) {`);
1168
1196
  propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
1169
1197
  propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
@@ -1188,14 +1216,14 @@ var generateObjectValidator = (schema, typeName) => {
1188
1216
  ].join(`
1189
1217
  `);
1190
1218
  };
1191
- var generateScalarValidator = (schema, typeName) => {
1219
+ var generateScalarValidator = (schema, typeName, suffix) => {
1192
1220
  const vName = validatorName(typeName);
1193
1221
  if (!isSchemaObject3(schema)) {
1194
1222
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
1195
1223
  `);
1196
1224
  }
1197
1225
  if (hasRef(schema)) {
1198
- const delegateName = validatorName(refToName(schema.$ref));
1226
+ const delegateName = validatorName(refToName(schema.$ref, suffix));
1199
1227
  return [
1200
1228
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1201
1229
  ` return ${delegateName}(input, _path)`,
@@ -1244,7 +1272,7 @@ var generateScalarValidator = (schema, typeName) => {
1244
1272
  const branches = schema.oneOf.map((branch, i) => {
1245
1273
  if (!hasRef(branch))
1246
1274
  return null;
1247
- const bName = validatorName(refToName(branch.$ref));
1275
+ const bName = validatorName(refToName(branch.$ref, suffix));
1248
1276
  return ` const _r${i} = ${bName}(input, _path)
1249
1277
  if (_r${i} === true) return true`;
1250
1278
  }).filter(Boolean).join(`
@@ -1314,21 +1342,23 @@ var generateScalarValidator = (schema, typeName) => {
1314
1342
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
1315
1343
  `);
1316
1344
  };
1317
- var generateValidatorFunction = (schema, typeName) => {
1345
+ var generateValidatorFunction = (schema, typeName, suffix = "") => {
1318
1346
  if (isObjectSchema2(schema)) {
1319
- return generateObjectValidator(schema, typeName);
1347
+ return generateObjectValidator(schema, typeName, suffix);
1320
1348
  }
1321
- return generateScalarValidator(schema, typeName);
1349
+ return generateScalarValidator(schema, typeName, suffix);
1322
1350
  };
1323
1351
 
1324
1352
  // src/generators/generate-files.ts
1325
1353
  var generateValidatorFile = (schema, typeName, options) => {
1354
+ const typeSuffix = options?.typeSuffix ?? "";
1326
1355
  const refImports = collectValidatorImports(schema, {
1327
1356
  selfRef: options?.selfRef,
1328
- rootSchema: options?.rootSchema
1357
+ rootSchema: options?.rootSchema,
1358
+ typeSuffix
1329
1359
  });
1330
- const typeDefinition = generateTypeDefinition(schema, typeName);
1331
- const validatorFunction = generateValidatorFunction(schema, typeName);
1360
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
1361
+ const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
1332
1362
  let result = `import type { ValidationResult, ValidationError } from './validation-result'
1333
1363
  `;
1334
1364
  for (const imp of refImports) {
@@ -1365,7 +1395,7 @@ export type ValidationError = {
1365
1395
  */
1366
1396
  export type ValidationResult = true | { valid: false; errors: ValidationError[] }
1367
1397
  `;
1368
- var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1398
+ var buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
1369
1399
  rootSchema = upgradeDraft07Schema(rootSchema);
1370
1400
  const files = [];
1371
1401
  const processedRefs = new Set;
@@ -1374,7 +1404,8 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1374
1404
  const dynamicRefMap = buildDynamicRefMap(rootSchema);
1375
1405
  const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
1376
1406
  const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
1377
- rootSchema
1407
+ rootSchema,
1408
+ typeSuffix
1378
1409
  });
1379
1410
  const rootFilename = rootTypeName.toLowerCase();
1380
1411
  if (rootFilename !== "validation-result") {
@@ -1393,12 +1424,13 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1393
1424
  console.warn(`Warning: Could not resolve ref: ${ref}`);
1394
1425
  continue;
1395
1426
  }
1396
- const typeName = refToName(ref);
1427
+ const typeName = refToName(ref, typeSuffix);
1397
1428
  const filename = refToFilename(ref);
1398
1429
  const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
1399
1430
  const content = generateValidatorFile(processedSchema, typeName, {
1400
1431
  selfRef: ref,
1401
- rootSchema
1432
+ rootSchema,
1433
+ typeSuffix
1402
1434
  });
1403
1435
  if (filename !== "validation-result" && !processedFilenames.has(filename)) {
1404
1436
  processedFilenames.add(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "json-schema-typed": "^8.0.1",
50
- "@amritk/helpers": "0.5.0"
50
+ "@amritk/helpers": "0.6.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@scalar/openapi-parser": "^0.26.1"
@@ -74,7 +74,7 @@ describe('build-schema', () => {
74
74
  const documentFile = files.find((f) => f.filename === 'document.ts')
75
75
 
76
76
  expect(documentFile?.content).toContain("from './info'")
77
- expect(documentFile?.content).toContain('validateInfoObject')
77
+ expect(documentFile?.content).toContain('validateInfo')
78
78
  })
79
79
 
80
80
  it('generates a valid index.ts with re-exports', async () => {
@@ -136,7 +136,7 @@ describe('build-schema', () => {
136
136
  const infoFile = files.find((f) => f.filename === 'info.ts')
137
137
 
138
138
  expect(documentFile?.content).toContain('validateDocument')
139
- expect(infoFile?.content).toContain('validateInfoObject')
139
+ expect(infoFile?.content).toContain('validateInfo')
140
140
 
141
141
  // Cross-check: @scalar/openapi-parser says a complete document is valid
142
142
  const validDoc = { openapi: '3.1.0', info: { title: 'API', version: '1.0' }, paths: {} }
@@ -55,7 +55,11 @@ export type ValidationResult = true | { valid: false; errors: ValidationError[]
55
55
  * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
56
56
  * ```
57
57
  */
58
- export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName: string): Promise<GeneratedFile[]> => {
58
+ export const buildValidatorSchema = async (
59
+ rootSchema: JSONSchema,
60
+ rootTypeName: string,
61
+ typeSuffix = '',
62
+ ): Promise<GeneratedFile[]> => {
59
63
  rootSchema = upgradeDraft07Schema(rootSchema as Record<string, unknown>) as JSONSchema
60
64
 
61
65
  const files: GeneratedFile[] = []
@@ -69,6 +73,7 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
69
73
  const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap)
70
74
  const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
71
75
  rootSchema: rootSchema as Record<string, unknown>,
76
+ typeSuffix,
72
77
  })
73
78
  const rootFilename = rootTypeName.toLowerCase()
74
79
 
@@ -91,12 +96,13 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
91
96
  continue
92
97
  }
93
98
 
94
- const typeName = refToName(ref)
99
+ const typeName = refToName(ref, typeSuffix)
95
100
  const filename = refToFilename(ref)
96
101
  const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap)
97
102
  const content = generateValidatorFile(processedSchema, typeName, {
98
103
  selfRef: ref,
99
104
  rootSchema: rootSchema as Record<string, unknown>,
105
+ typeSuffix,
100
106
  })
101
107
 
102
108
  if (filename !== 'validation-result' && !processedFilenames.has(filename)) {
@@ -18,15 +18,20 @@ type CollectValidatorImportsOptions = {
18
18
  * are excluded from the import list (they were never generated as files).
19
19
  */
20
20
  readonly rootSchema?: Record<string, unknown> | undefined
21
+ /**
22
+ * Suffix appended to every type/validator name derived from a `$ref`. Must
23
+ * match the suffix used when generating the referenced files. Defaults to `''`.
24
+ */
25
+ readonly typeSuffix?: string
21
26
  }
22
27
 
23
28
  /**
24
29
  * Generates an import statement for a single $ref, importing both the type
25
30
  * and the validator function from the ref's generated file.
26
31
  */
27
- const buildImport = (ref: string): string => {
32
+ const buildImport = (ref: string, suffix: string): string => {
28
33
  const filename = refToFilename(ref)
29
- const typeName = refToName(ref)
34
+ const typeName = refToName(ref, suffix)
30
35
  const validatorName = `validate${typeName}`
31
36
  return `import { type ${typeName}, ${validatorName} } from './${filename}'`
32
37
  }
@@ -94,12 +99,13 @@ const collectDirectRefs = (schema: JSONSchema): string[] => {
94
99
  * ```typescript
95
100
  * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
96
101
  * collectValidatorImports(schema)
97
- * // ["import { type ContactObject, validateContactObject } from './contact-object'"]
102
+ * // ["import { type Contact, validateContact } from './contact'"]
98
103
  * ```
99
104
  */
100
105
  export const collectValidatorImports = (schema: JSONSchema, options?: CollectValidatorImportsOptions): string[] => {
101
106
  const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null
102
107
  const rootSchema = options?.rootSchema
108
+ const typeSuffix = options?.typeSuffix ?? ''
103
109
 
104
110
  const refs = collectDirectRefs(schema)
105
111
  const seen = new Set<string>()
@@ -121,7 +127,7 @@ export const collectValidatorImports = (schema: JSONSchema, options?: CollectVal
121
127
 
122
128
  // -or-reference unions import the base type's validator
123
129
  const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref
124
- imports.push(buildImport(importRef))
130
+ imports.push(buildImport(importRef, typeSuffix))
125
131
  }
126
132
 
127
133
  return imports
@@ -17,6 +17,11 @@ type GenerateValidatorFileOptions = {
17
17
  * The root schema document. Used to filter out unresolvable refs.
18
18
  */
19
19
  readonly rootSchema?: Record<string, unknown>
20
+ /**
21
+ * Suffix appended to every type/validator name derived from a `$ref`.
22
+ * Defaults to `''` (no suffix).
23
+ */
24
+ readonly typeSuffix?: string
20
25
  }
21
26
 
22
27
  /**
@@ -46,13 +51,15 @@ export const generateValidatorFile = (
46
51
  typeName: string,
47
52
  options?: GenerateValidatorFileOptions,
48
53
  ): string => {
54
+ const typeSuffix = options?.typeSuffix ?? ''
49
55
  const refImports = collectValidatorImports(schema, {
50
56
  selfRef: options?.selfRef,
51
57
  rootSchema: options?.rootSchema,
58
+ typeSuffix,
52
59
  })
53
60
 
54
- const typeDefinition = generateTypeDefinition(schema, typeName)
55
- const validatorFunction = generateValidatorFunction(schema, typeName)
61
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix })
62
+ const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix)
56
63
 
57
64
  let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`
58
65
 
@@ -135,7 +135,7 @@ describe('generate-validator-function', () => {
135
135
  }
136
136
  const code = generateValidatorFunction(schema, 'Document')
137
137
 
138
- expect(code).toContain('validateInfoObject(')
138
+ expect(code).toContain('validateInfo(')
139
139
  expect(code).toContain('"info" in obj')
140
140
  })
141
141
 
@@ -61,7 +61,7 @@ const wrongTypeCondition = (accessor: string, type: string): string => {
61
61
  * Generates validation lines for a single property in an object schema.
62
62
  * Handles $ref delegation, enum checks, type checks, and string/number constraints.
63
63
  */
64
- const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean): string[] => {
64
+ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean, suffix: string): string[] => {
65
65
  if (!isSchemaObject(propSchema)) return []
66
66
 
67
67
  const raw = `obj[${JSON.stringify(key)}]`
@@ -71,7 +71,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
71
71
  // $ref — delegate to the imported validator
72
72
  if (hasRef(propSchema)) {
73
73
  const ref = propSchema.$ref
74
- const vName = validatorName(refToName(ref))
74
+ const vName = validatorName(refToName(ref, suffix))
75
75
 
76
76
  if (isRequired) {
77
77
  lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
@@ -218,7 +218,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
218
218
  if (t === 'array' && hasItems(propSchema)) {
219
219
  const itemSchema = propSchema.items
220
220
  if (hasRef(itemSchema)) {
221
- const vName = validatorName(refToName(itemSchema.$ref))
221
+ const vName = validatorName(refToName(itemSchema.$ref, suffix))
222
222
  lines.push(` if (Array.isArray(${raw})) {`)
223
223
  lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`)
224
224
  lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`)
@@ -250,7 +250,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
250
250
  * Generates a validator function body for an object schema, checking each
251
251
  * property's presence and type and collecting all errors.
252
252
  */
253
- const generateObjectValidator = (schema: JSONSchema, typeName: string): string => {
253
+ const generateObjectValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
254
254
  const vName = validatorName(typeName)
255
255
  const required = new Set(hasRequired(schema) ? schema.required : [])
256
256
  const properties = hasProperties(schema) ? schema.properties : {}
@@ -258,7 +258,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
258
258
  const propertyLines: string[] = []
259
259
 
260
260
  for (const [key, propSchema] of Object.entries(properties)) {
261
- const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key))
261
+ const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key), suffix)
262
262
  if (checks.length > 0) {
263
263
  propertyLines.push(...checks)
264
264
  }
@@ -270,7 +270,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
270
270
  isSchemaObject(schema.additionalProperties) &&
271
271
  hasRef(schema.additionalProperties)
272
272
  ) {
273
- const vRefName = validatorName(refToName(schema.additionalProperties.$ref))
273
+ const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix))
274
274
  propertyLines.push(` for (const _key of Object.keys(obj)) {`)
275
275
  propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`)
276
276
  propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`)
@@ -297,7 +297,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
297
297
  /**
298
298
  * Generates a validator function for a non-object schema (primitive, array, enum, $ref).
299
299
  */
300
- const generateScalarValidator = (schema: JSONSchema, typeName: string): string => {
300
+ const generateScalarValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
301
301
  const vName = validatorName(typeName)
302
302
 
303
303
  if (!isSchemaObject(schema)) {
@@ -308,7 +308,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
308
308
 
309
309
  // Top-level $ref — delegate entirely
310
310
  if (hasRef(schema)) {
311
- const delegateName = validatorName(refToName(schema.$ref))
311
+ const delegateName = validatorName(refToName(schema.$ref, suffix))
312
312
  return [
313
313
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
314
314
  ` return ${delegateName}(input, _path)`,
@@ -361,7 +361,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
361
361
  const branches = schema.oneOf
362
362
  .map((branch, i) => {
363
363
  if (!hasRef(branch)) return null
364
- const bName = validatorName(refToName((branch as { $ref: string }).$ref))
364
+ const bName = validatorName(refToName((branch as { $ref: string }).$ref, suffix))
365
365
  return ` const _r${i} = ${bName}(input, _path)\n if (_r${i} === true) return true`
366
366
  })
367
367
  .filter(Boolean)
@@ -463,10 +463,10 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
463
463
  * // }
464
464
  * ```
465
465
  */
466
- export const generateValidatorFunction = (schema: JSONSchema, typeName: string): string => {
466
+ export const generateValidatorFunction = (schema: JSONSchema, typeName: string, suffix = ''): string => {
467
467
  if (isObjectSchema(schema)) {
468
- return generateObjectValidator(schema, typeName)
468
+ return generateObjectValidator(schema, typeName, suffix)
469
469
  }
470
470
 
471
- return generateScalarValidator(schema, typeName)
471
+ return generateScalarValidator(schema, typeName, suffix)
472
472
  }