@gabrielbryk/json-schema-to-zod 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @gabrielbryk/json-schema-to-zod
2
2
 
3
+ ## 2.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ff1858d: Add naming customization hooks to jsonSchemaToZod output for schema and type exports.
8
+
3
9
  ## 2.13.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -92,6 +92,21 @@ export const mySchema = z.object({ hello: z.string().optional() });
92
92
  export type MySchema = z.infer<typeof mySchema>;
93
93
  ```
94
94
 
95
+ #### Naming customization
96
+
97
+ Use the `naming` option to control schema const names and exported type names (root + lifted refs).
98
+
99
+ ```typescript
100
+ const moduleWithCustomNaming = jsonSchemaToZod(myObject, {
101
+ name: "Workflow",
102
+ typeExports: true,
103
+ naming: {
104
+ schemaName: (base) => base,
105
+ typeName: (base) => `${base}Type`,
106
+ },
107
+ });
108
+ ```
109
+
95
110
  #### Example with `$refs` resolved and output formatted
96
111
 
97
112
  ```typescript
@@ -1,23 +1,38 @@
1
1
  import { parseSchema } from "../parsers/parseSchema.js";
2
2
  import { detectCycles, computeScc } from "../utils/cycles.js";
3
3
  import { buildRefRegistry } from "../utils/buildRefRegistry.js";
4
+ import { resolveSchemaName } from "../utils/schemaNaming.js";
4
5
  export const analyzeSchema = (schema, options = {}) => {
5
- const { name, type, ...rest } = options;
6
+ const { name, type, naming, ...rest } = options;
6
7
  if (type && !name) {
7
8
  throw new Error("Option `type` requires `name` to be set");
8
9
  }
10
+ const nameContext = { isRoot: true, isLifted: false };
11
+ const rootBaseName = name;
12
+ let resolvedName = name;
13
+ const usedNames = new Set();
14
+ const usedBaseNames = new Set();
15
+ if (name && naming) {
16
+ resolvedName = resolveSchemaName(name, naming, nameContext, usedNames);
17
+ usedBaseNames.add(name);
18
+ }
19
+ if (resolvedName) {
20
+ usedNames.add(resolvedName);
21
+ }
9
22
  const normalized = {
10
- name,
23
+ name: resolvedName,
11
24
  type,
25
+ naming,
12
26
  module: "esm",
13
27
  ...rest,
14
28
  exportRefs: rest.exportRefs ?? true,
15
29
  withMeta: rest.withMeta ?? true,
16
30
  };
17
31
  const refNameByPointer = new Map();
18
- const usedNames = new Set();
19
- if (name) {
20
- usedNames.add(name);
32
+ const refBaseNameByPointer = new Map();
33
+ const baseNameBySchema = new Map();
34
+ if (naming && resolvedName && rootBaseName) {
35
+ baseNameBySchema.set(resolvedName, rootBaseName);
21
36
  }
22
37
  const declarations = new Map();
23
38
  const dependencies = new Map();
@@ -37,13 +52,17 @@ export const analyzeSchema = (schema, options = {}) => {
37
52
  dependencies,
38
53
  inProgress: new Set(),
39
54
  refNameByPointer,
55
+ refBaseNameByPointer,
56
+ baseNameBySchema,
40
57
  usedNames,
58
+ usedBaseNames,
41
59
  root: schema,
42
- currentSchemaName: name,
60
+ currentSchemaName: resolvedName,
43
61
  refRegistry,
44
62
  rootBaseUri,
45
63
  ...rest,
46
64
  withMeta: normalized.withMeta,
65
+ naming,
47
66
  };
48
67
  parseSchema(schema, pass1);
49
68
  const names = Array.from(declarations.keys());
@@ -71,6 +90,9 @@ export const analyzeSchema = (schema, options = {}) => {
71
90
  definitions: {}, // Legacy support
72
91
  dependencies,
73
92
  refNameByPointer,
93
+ refBaseNameByPointer,
94
+ baseNameBySchema,
95
+ rootBaseName,
74
96
  usedNames,
75
97
  cycleRefNames,
76
98
  cycleComponentByName: componentByName,
@@ -2,6 +2,7 @@ import { parseSchema } from "../parsers/parseSchema.js";
2
2
  import { expandJsdocs } from "../utils/jsdocs.js";
3
3
  import { inferTypeFromExpression } from "../utils/schemaRepresentation.js";
4
4
  import { EsmEmitter } from "../utils/esmEmitter.js";
5
+ import { resolveTypeName } from "../utils/schemaNaming.js";
5
6
  /**
6
7
  * Split a z.object({...}).method1().method2() expression into base and method chain.
7
8
  * This is needed for Rule 2: Don't chain methods on recursive types.
@@ -148,8 +149,8 @@ const orderDeclarations = (entries, dependencies) => {
148
149
  return ordered.map((name) => [name, repByName.get(name)]);
149
150
  };
150
151
  export const emitZod = (analysis) => {
151
- const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName } = analysis;
152
- const { name, type, noImport, exportRefs, typeExports, withMeta, ...rest } = options;
152
+ const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName, baseNameBySchema, rootBaseName, } = analysis;
153
+ const { name, type, naming, noImport, exportRefs, typeExports, withMeta, ...rest } = options;
153
154
  const declarations = new Map();
154
155
  const dependencies = new Map();
155
156
  // Fresh name registry for the emission pass.
@@ -175,6 +176,7 @@ export const emitZod = (analysis) => {
175
176
  rootBaseUri: analysis.rootBaseUri,
176
177
  ...rest,
177
178
  withMeta,
179
+ naming,
178
180
  });
179
181
  const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
180
182
  ? expandJsdocs(typeof schema.description === "string"
@@ -182,6 +184,13 @@ export const emitZod = (analysis) => {
182
184
  : "")
183
185
  : "";
184
186
  const emitter = new EsmEmitter();
187
+ const usedTypeNames = new Set();
188
+ const resolveDeclarationTypeName = (schemaName) => {
189
+ if (!naming)
190
+ return schemaName;
191
+ const baseName = baseNameBySchema.get(schemaName) ?? schemaName;
192
+ return resolveTypeName(baseName, naming, { isRoot: false, isLifted: true }, usedTypeNames);
193
+ };
185
194
  if (!noImport) {
186
195
  emitter.addNamedImport("z", "zod");
187
196
  }
@@ -236,10 +245,14 @@ export const emitZod = (analysis) => {
236
245
  });
237
246
  // Export type for this declaration if typeExports is enabled
238
247
  if (typeExports && exportRefs) {
248
+ const typeName = resolveDeclarationTypeName(refName);
239
249
  emitter.addTypeExport({
240
- name: refName,
250
+ name: typeName ?? refName,
241
251
  type: `z.infer<typeof ${refName}>`,
242
252
  });
253
+ if (typeName) {
254
+ usedTypeNames.add(typeName);
255
+ }
243
256
  }
244
257
  continue;
245
258
  }
@@ -252,10 +265,14 @@ export const emitZod = (analysis) => {
252
265
  });
253
266
  // Export type for this declaration if typeExports is enabled
254
267
  if (typeExports && exportRefs) {
268
+ const typeName = resolveDeclarationTypeName(refName);
255
269
  emitter.addTypeExport({
256
- name: refName,
270
+ name: typeName ?? refName,
257
271
  type: `z.infer<typeof ${refName}>`,
258
272
  });
273
+ if (typeName) {
274
+ usedTypeNames.add(typeName);
275
+ }
259
276
  }
260
277
  }
261
278
  }
@@ -275,11 +292,18 @@ export const emitZod = (analysis) => {
275
292
  }
276
293
  // Export type for root schema if type option is set, or if typeExports is enabled
277
294
  if (name && (type || typeExports)) {
278
- const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
279
- emitter.addTypeExport({
280
- name: typeName,
281
- type: `z.infer<typeof ${name}>`,
282
- });
295
+ const rootTypeName = typeof type === "string"
296
+ ? type
297
+ : naming && rootBaseName
298
+ ? resolveTypeName(rootBaseName, naming, { isRoot: true, isLifted: false }, usedTypeNames)
299
+ : `${name[0].toUpperCase()}${name.substring(1)}`;
300
+ if (rootTypeName) {
301
+ emitter.addTypeExport({
302
+ name: rootTypeName,
303
+ type: `z.infer<typeof ${name}>`,
304
+ });
305
+ usedTypeNames.add(rootTypeName);
306
+ }
283
307
  }
284
308
  return emitter.render();
285
309
  };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ export * from "./utils/namingService.js";
33
33
  export * from "./utils/omit.js";
34
34
  export * from "./utils/resolveRef.js";
35
35
  export * from "./utils/resolveUri.js";
36
+ export * from "./utils/schemaNaming.js";
36
37
  export * from "./utils/schemaRepresentation.js";
37
38
  export * from "./utils/withMessage.js";
38
39
  export * from "./zodToJsonSchema.js";
@@ -18,6 +18,7 @@ import { parseNullable } from "./parseNullable.js";
18
18
  import { anyOrUnknown } from "../utils/anyOrUnknown.js";
19
19
  import { resolveUri } from "../utils/resolveUri.js";
20
20
  import { resolveRef } from "../utils/resolveRef.js";
21
+ import { ensureUnique, resolveSchemaName, sanitizeIdentifier } from "../utils/schemaNaming.js";
21
22
  export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockMeta) => {
22
23
  // Ensure ref bookkeeping exists so $ref declarations and getter-based recursion work
23
24
  refs.root = refs.root ?? schema;
@@ -253,10 +254,20 @@ const getOrCreateRefName = (pointer, path, refs) => {
253
254
  if (refs.refNameByPointer?.has(pointer)) {
254
255
  return refs.refNameByPointer.get(pointer);
255
256
  }
256
- const preferred = buildNameFromPath(path, refs.usedNames);
257
- refs.refNameByPointer?.set(pointer, preferred);
258
- refs.usedNames?.add(preferred);
259
- return preferred;
257
+ if (!refs.naming) {
258
+ const preferred = buildNameFromPath(path, refs.usedNames);
259
+ refs.refNameByPointer?.set(pointer, preferred);
260
+ refs.usedNames?.add(preferred);
261
+ return preferred;
262
+ }
263
+ const baseName = buildBaseNameFromPath(path, refs.usedBaseNames);
264
+ const schemaName = resolveSchemaName(baseName, refs.naming, { isRoot: false, isLifted: true }, refs.usedNames);
265
+ refs.refNameByPointer?.set(pointer, schemaName);
266
+ refs.refBaseNameByPointer?.set(pointer, baseName);
267
+ refs.baseNameBySchema?.set(schemaName, baseName);
268
+ refs.usedNames?.add(schemaName);
269
+ refs.usedBaseNames?.add(baseName);
270
+ return schemaName;
260
271
  };
261
272
  const buildNameFromPath = (path, used) => {
262
273
  const filtered = path
@@ -289,19 +300,36 @@ const buildNameFromPath = (path, used) => {
289
300
  finalName += "Schema";
290
301
  }
291
302
  const sanitized = sanitizeIdentifier(finalName);
292
- if (!used || !used.has(sanitized))
293
- return sanitized;
294
- let counter = 2;
295
- let candidate = `${sanitized}${counter}`;
296
- while (used.has(candidate)) {
297
- counter += 1;
298
- candidate = `${sanitized}${counter}`;
299
- }
300
- return candidate;
303
+ return ensureUnique(sanitized, used);
301
304
  };
302
- const sanitizeIdentifier = (value) => {
303
- const cleaned = value.replace(/^[^a-zA-Z_$]+/, "").replace(/[^a-zA-Z0-9_$]/g, "");
304
- return cleaned || "Ref";
305
+ const buildBaseNameFromPath = (path, used) => {
306
+ const filtered = path
307
+ .map((segment, idx) => {
308
+ if (idx === 0 && (segment === "$defs" || segment === "definitions")) {
309
+ return undefined; // root-level defs prefix is redundant for naming
310
+ }
311
+ if (segment === "properties")
312
+ return undefined; // skip noisy properties segment
313
+ if (segment === "$defs" || segment === "definitions")
314
+ return "Defs";
315
+ return segment;
316
+ })
317
+ .filter((segment) => segment !== undefined);
318
+ const base = filtered.length
319
+ ? filtered
320
+ .map((segment) => typeof segment === "number"
321
+ ? `Ref${segment}`
322
+ : segment
323
+ .toString()
324
+ .replace(/[^a-zA-Z0-9_$]/g, " ")
325
+ .split(" ")
326
+ .filter(Boolean)
327
+ .map(capitalize)
328
+ .join(""))
329
+ .join("")
330
+ : "Ref";
331
+ const sanitized = sanitizeIdentifier(base);
332
+ return ensureUnique(sanitized, used);
305
333
  };
306
334
  const capitalize = (value) => value.length ? value[0].toUpperCase() + value.slice(1) : value;
307
335
  const addDefaults = (schema, parsed) => {
@@ -73,6 +73,16 @@ export type JsonSchemaObject = {
73
73
  } & Record<string, unknown>;
74
74
  export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => SchemaRepresentation;
75
75
  export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => string | void;
76
+ export type NamingContext = {
77
+ isRoot: boolean;
78
+ isLifted: boolean;
79
+ };
80
+ export type NamingOptions = {
81
+ /** Customize the const name for schemas. Defaults to appending "Schema". */
82
+ schemaName?: (baseName: string, ctx: NamingContext) => string;
83
+ /** Customize the type name for schemas. Defaults to baseName when naming is enabled. */
84
+ typeName?: (baseName: string, ctx: NamingContext) => string | undefined;
85
+ };
76
86
  export type Options = {
77
87
  name?: string;
78
88
  withoutDefaults?: boolean;
@@ -80,6 +90,8 @@ export type Options = {
80
90
  withJsdocs?: boolean;
81
91
  /** Use .meta() instead of .describe() - includes id, title, description */
82
92
  withMeta?: boolean;
93
+ /** Customize schema and type naming for root and lifted schemas. */
94
+ naming?: NamingOptions;
83
95
  parserOverride?: ParserOverride;
84
96
  depth?: number;
85
97
  type?: boolean | string;
@@ -184,7 +196,10 @@ export type Refs = Options & {
184
196
  dependencies?: Map<string, Set<string>>;
185
197
  inProgress?: Set<string>;
186
198
  refNameByPointer?: Map<string, string>;
199
+ refBaseNameByPointer?: Map<string, string>;
200
+ baseNameBySchema?: Map<string, string>;
187
201
  usedNames?: Set<string>;
202
+ usedBaseNames?: Set<string>;
188
203
  currentSchemaName?: string;
189
204
  cycleRefNames?: Set<string>;
190
205
  cycleComponentByName?: Map<string, number>;
@@ -8,6 +8,9 @@ export type AnalysisResult = {
8
8
  schema: JsonSchema;
9
9
  options: NormalizedOptions;
10
10
  refNameByPointer: Map<string, string>;
11
+ refBaseNameByPointer: Map<string, string>;
12
+ baseNameBySchema: Map<string, string>;
13
+ rootBaseName?: string;
11
14
  usedNames: Set<string>;
12
15
  declarations: Map<string, SchemaRepresentation>;
13
16
  dependencies: Map<string, Set<string>>;
@@ -33,6 +33,7 @@ export * from "./utils/namingService.js";
33
33
  export * from "./utils/omit.js";
34
34
  export * from "./utils/resolveRef.js";
35
35
  export * from "./utils/resolveUri.js";
36
+ export * from "./utils/schemaNaming.js";
36
37
  export * from "./utils/schemaRepresentation.js";
37
38
  export * from "./utils/withMessage.js";
38
39
  export * from "./zodToJsonSchema.js";
@@ -0,0 +1,6 @@
1
+ import { NamingContext, NamingOptions } from "../Types.js";
2
+ export declare const defaultSchemaName: (baseName: string) => string;
3
+ export declare const sanitizeIdentifier: (value: string) => string;
4
+ export declare const ensureUnique: (candidate: string, used?: Set<string>) => string;
5
+ export declare const resolveSchemaName: (baseName: string, naming: NamingOptions, ctx: NamingContext, used?: Set<string>) => string;
6
+ export declare const resolveTypeName: (baseName: string, naming: NamingOptions, ctx: NamingContext, used?: Set<string>) => string | undefined;
@@ -0,0 +1,31 @@
1
+ export const defaultSchemaName = (baseName) => `${baseName}Schema`;
2
+ export const sanitizeIdentifier = (value) => {
3
+ const cleaned = value.replace(/^[^a-zA-Z_$]+/, "").replace(/[^a-zA-Z0-9_$]/g, "");
4
+ return cleaned || "Ref";
5
+ };
6
+ export const ensureUnique = (candidate, used) => {
7
+ if (!used)
8
+ return candidate;
9
+ if (!used.has(candidate))
10
+ return candidate;
11
+ let counter = 2;
12
+ let unique = `${candidate}${counter}`;
13
+ while (used.has(unique)) {
14
+ counter += 1;
15
+ unique = `${candidate}${counter}`;
16
+ }
17
+ return unique;
18
+ };
19
+ export const resolveSchemaName = (baseName, naming, ctx, used) => {
20
+ const schemaNameFn = naming.schemaName ?? defaultSchemaName;
21
+ const candidate = sanitizeIdentifier(schemaNameFn(baseName, ctx));
22
+ return ensureUnique(candidate, used);
23
+ };
24
+ export const resolveTypeName = (baseName, naming, ctx, used) => {
25
+ const typeNameFn = naming.typeName ?? ((name) => name);
26
+ const candidate = typeNameFn(baseName, ctx);
27
+ if (!candidate)
28
+ return undefined;
29
+ const sanitized = sanitizeIdentifier(candidate);
30
+ return ensureUnique(sanitized, used);
31
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gabrielbryk/json-schema-to-zod",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Converts JSON schema objects or files into Zod schemas",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",