@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 +6 -0
- package/README.md +15 -0
- package/dist/core/analyzeSchema.js +28 -6
- package/dist/core/emitZod.js +33 -9
- package/dist/index.js +1 -0
- package/dist/parsers/parseSchema.js +44 -16
- package/dist/types/Types.d.ts +15 -0
- package/dist/types/core/analyzeSchema.d.ts +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/utils/schemaNaming.d.ts +6 -0
- package/dist/utils/schemaNaming.js +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
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,
|
package/dist/core/emitZod.js
CHANGED
|
@@ -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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
303
|
-
const
|
|
304
|
-
|
|
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) => {
|
package/dist/types/Types.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/types/index.d.ts
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";
|
|
@@ -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
|
+
};
|