@davars/graphql-codegen-zod 0.1.0 → 0.3.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 (3) hide show
  1. package/dist/zod.d.ts +13 -0
  2. package/dist/zod.js +63 -26
  3. package/package.json +11 -8
package/dist/zod.d.ts CHANGED
@@ -6,11 +6,24 @@ import { BaseSchemaVisitor } from './schema_visitor.js';
6
6
  export declare class ZodSchemaVisitor extends BaseSchemaVisitor {
7
7
  private resolvedTransforms;
8
8
  private usedTransformImports;
9
+ /** GraphQL type names whose schemas had annotations dropped (divergent output types). */
10
+ private droppedAnnotationTypes;
11
+ /** Whether the definedNonNullAnySchema fallback was referenced during generation. */
12
+ private usedAnySchema;
13
+ /** Called by zod4Scalar when the anySchema fallback is used. */
14
+ markAnySchemaUsed(): void;
9
15
  constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig, resolvedTransforms?: Record<string, ResolvedTransform>);
10
16
  private addTransformImport;
11
17
  buildImports(): string[];
12
18
  private getTransformForGraphQLName;
13
19
  private hasTransformedFieldRef;
20
+ /**
21
+ * Check if any field references a type whose schema output diverges from the
22
+ * TypeScript type. This includes direct custom scalar fields (e.g. z.coerce.date()
23
+ * outputs Date but TS says string) and transitive references to types that already
24
+ * had their annotations dropped.
25
+ */
26
+ private hasDivergentFieldRef;
14
27
  importValidationSchema(): string;
15
28
  initialEmit(): string;
16
29
  get InputObjectTypeDefinition(): {
package/dist/zod.js CHANGED
@@ -8,6 +8,14 @@ const anySchema = `definedNonNullAnySchema`;
8
8
  export class ZodSchemaVisitor extends BaseSchemaVisitor {
9
9
  resolvedTransforms;
10
10
  usedTransformImports = new Map();
11
+ /** GraphQL type names whose schemas had annotations dropped (divergent output types). */
12
+ droppedAnnotationTypes = new Set();
13
+ /** Whether the definedNonNullAnySchema fallback was referenced during generation. */
14
+ usedAnySchema = false;
15
+ /** Called by zod4Scalar when the anySchema fallback is used. */
16
+ markAnySchemaUsed() {
17
+ this.usedAnySchema = true;
18
+ }
11
19
  constructor(schema, config, resolvedTransforms = {}) {
12
20
  super(schema, config);
13
21
  this.resolvedTransforms = resolvedTransforms;
@@ -18,6 +26,8 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
18
26
  this.usedTransformImports.set(transform.importPath, existing);
19
27
  }
20
28
  buildImports() {
29
+ // Filter out types whose annotations were dropped (they'd be imported but unused)
30
+ this.importTypes = this.importTypes.filter(t => !this.droppedAnnotationTypes.has(t));
21
31
  const baseImports = super.buildImports();
22
32
  const transformImports = Array.from(this.usedTransformImports.entries())
23
33
  .map(([modulePath, symbols]) => `import { ${Array.from(symbols).sort().join(', ')} } from '${modulePath}'`);
@@ -36,34 +46,54 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
36
46
  return type.kind === Kind.NAMED_TYPE && this.resolvedTransforms[type.name.value] !== undefined;
37
47
  });
38
48
  }
49
+ /**
50
+ * Check if any field references a type whose schema output diverges from the
51
+ * TypeScript type. This includes direct custom scalar fields (e.g. z.coerce.date()
52
+ * outputs Date but TS says string) and transitive references to types that already
53
+ * had their annotations dropped.
54
+ */
55
+ hasDivergentFieldRef(fields) {
56
+ if (!fields)
57
+ return false;
58
+ return fields.some((field) => {
59
+ let type = field.type;
60
+ while (type.kind === Kind.NON_NULL_TYPE || type.kind === Kind.LIST_TYPE)
61
+ type = type.type;
62
+ if (type.kind !== Kind.NAMED_TYPE)
63
+ return false;
64
+ const name = type.name.value;
65
+ return (this.config.scalarSchemas?.[name] !== undefined) || this.droppedAnnotationTypes.has(name);
66
+ });
67
+ }
39
68
  importValidationSchema() {
40
69
  return `import * as z from 'zod'`;
41
70
  }
42
71
  initialEmit() {
43
- return (`\n${[
72
+ const blocks = [
44
73
  new DeclarationBlock({})
45
74
  .asKind('type')
46
75
  .withName('Properties<T>')
47
76
  .withContent(['Required<{', ' [K in keyof T]: z.ZodType<T[K], T[K]>;', '}>'].join('\n'))
48
77
  .string,
78
+ ];
79
+ if (this.usedAnySchema) {
49
80
  // Unfortunately, zod doesn't provide non-null defined any schema.
50
81
  // This is a temporary hack until it is fixed.
51
82
  // see: https://github.com/colinhacks/zod/issues/884
52
- new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string,
53
- new DeclarationBlock({})
83
+ blocks.push(new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, new DeclarationBlock({})
54
84
  .export()
55
85
  .asKind('const')
56
86
  .withName(`isDefinedNonNullAny`)
57
87
  .withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`)
58
- .string,
59
- new DeclarationBlock({})
88
+ .string, new DeclarationBlock({})
60
89
  .export()
61
90
  .asKind('const')
62
91
  .withName(`${anySchema}`)
63
92
  .withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`)
64
- .string,
65
- ...this.enumDeclarations,
66
- ].join('\n')}`);
93
+ .string);
94
+ }
95
+ blocks.push(...this.enumDeclarations);
96
+ return `\n${blocks.join('\n')}`;
67
97
  }
68
98
  get InputObjectTypeDefinition() {
69
99
  return {
@@ -90,9 +120,11 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
90
120
  const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
91
121
  const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';
92
122
  // Building schema for fields.
93
- const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
123
+ const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this)).join(',\n');
94
124
  const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
95
- const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields);
125
+ const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields) || this.hasDivergentFieldRef(node.fields);
126
+ if (dropAnnotation)
127
+ this.droppedAnnotationTypes.add(node.name.value);
96
128
  const schemaName = dropAnnotation
97
129
  ? `${name}Schema`
98
130
  : `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
@@ -131,12 +163,14 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
131
163
  const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
132
164
  const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';
133
165
  // Building schema for fields.
134
- const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
166
+ const shape = node.fields?.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this)).join(',\n');
135
167
  const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
136
168
  // Drop the explicit type annotation when a transform is applied (changes
137
169
  // return type from ZodObject to ZodPipe) or when any field references a
138
170
  // transformed type (input/output types diverge, breaking Properties<T>).
139
- const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields);
171
+ const dropAnnotation = !!transform || this.hasTransformedFieldRef(node.fields) || this.hasDivergentFieldRef(node.fields);
172
+ if (dropAnnotation)
173
+ this.droppedAnnotationTypes.add(node.name.value);
140
174
  const schemaName = dropAnnotation
141
175
  ? `${name}Schema`
142
176
  : `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
@@ -148,7 +182,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
148
182
  .withName(schemaName)
149
183
  .withContent([
150
184
  `z.object({`,
151
- indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
185
+ indent(`__typename: z.literal('${node.name.value}'),`, 2),
152
186
  shape,
153
187
  `})${transformSuffix}`,
154
188
  ].join('\n'))
@@ -161,7 +195,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
161
195
  .withName(dropAnnotation ? `${name}Schema()` : `${name}Schema(): z.ZodObject<Properties<${typeName}>>`)
162
196
  .withBlock([
163
197
  indent(`return z.object({`),
164
- indent(`__typename: z.literal('${node.name.value}').optional(),`, 2),
198
+ indent(`__typename: z.literal('${node.name.value}'),`, 2),
165
199
  shape,
166
200
  indent(`})${transformSuffix}`),
167
201
  ].join('\n'))
@@ -228,13 +262,15 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
228
262
  }
229
263
  buildInputFields(fields, visitor, name, graphqlName) {
230
264
  const typeName = visitor.prefixTypeNamespace(name);
231
- const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n');
265
+ const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2, this)).join(',\n');
232
266
  const transform = graphqlName ? this.getTransformForGraphQLName(graphqlName) : undefined;
233
267
  if (transform) {
234
268
  this.addTransformImport(transform);
235
269
  }
236
270
  const transformSuffix = transform ? `.transform(${transform.symbolName})` : '';
237
- const dropAnnotation = !!transform || this.hasTransformedFieldRef(fields);
271
+ const dropAnnotation = !!transform || this.hasTransformedFieldRef(fields) || this.hasDivergentFieldRef(fields);
272
+ if (dropAnnotation && graphqlName)
273
+ this.droppedAnnotationTypes.add(graphqlName);
238
274
  const schemaName = dropAnnotation
239
275
  ? `${name}Schema`
240
276
  : `${name}Schema: z.ZodObject<Properties<${typeName}>>`;
@@ -257,13 +293,13 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
257
293
  }
258
294
  }
259
295
  }
260
- function generateFieldZodSchema(config, visitor, field, indentCount) {
261
- const gen = generateFieldTypeZodSchema(config, visitor, field, field.type);
296
+ function generateFieldZodSchema(config, visitor, field, indentCount, schemaVisitor) {
297
+ const gen = generateFieldTypeZodSchema(config, visitor, field, field.type, undefined, schemaVisitor);
262
298
  return indent(`${field.name.value}: ${maybeLazy(visitor, field.type, gen)}`, indentCount);
263
299
  }
264
- function generateFieldTypeZodSchema(config, visitor, field, type, parentType) {
300
+ function generateFieldTypeZodSchema(config, visitor, field, type, parentType, schemaVisitor) {
265
301
  if (isListType(type)) {
266
- const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type);
302
+ const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, schemaVisitor);
267
303
  if (!isNonNullType(parentType)) {
268
304
  const arrayGen = `z.array(${maybeLazy(visitor, type.type, gen)})`;
269
305
  const maybeLazyGen = applyDirectives(config, field, arrayGen);
@@ -272,11 +308,11 @@ function generateFieldTypeZodSchema(config, visitor, field, type, parentType) {
272
308
  return `z.array(${maybeLazy(visitor, type.type, gen)})`;
273
309
  }
274
310
  if (isNonNullType(type)) {
275
- const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type);
311
+ const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type, schemaVisitor);
276
312
  return maybeLazy(visitor, type.type, gen);
277
313
  }
278
314
  if (isNamedType(type)) {
279
- const gen = generateNameNodeZodSchema(config, visitor, type.name);
315
+ const gen = generateNameNodeZodSchema(config, visitor, type.name, schemaVisitor);
280
316
  if (isListType(parentType))
281
317
  return `${gen}.nullable()`;
282
318
  let appliedDirectivesGen = applyDirectives(config, field, gen);
@@ -315,7 +351,7 @@ function applyDirectives(config, field, gen) {
315
351
  }
316
352
  return gen;
317
353
  }
318
- function generateNameNodeZodSchema(config, visitor, node) {
354
+ function generateNameNodeZodSchema(config, visitor, node, schemaVisitor) {
319
355
  const converter = visitor.getNameNodeConverter(node);
320
356
  switch (converter?.targetKind) {
321
357
  case 'InterfaceTypeDefinition':
@@ -333,11 +369,11 @@ function generateNameNodeZodSchema(config, visitor, node) {
333
369
  case 'EnumTypeDefinition':
334
370
  return `${converter.convertName()}Schema`;
335
371
  case 'ScalarTypeDefinition':
336
- return zod4Scalar(config, visitor, node.value);
372
+ return zod4Scalar(config, visitor, node.value, schemaVisitor);
337
373
  default:
338
374
  if (converter?.targetKind)
339
375
  console.warn('Unknown targetKind', converter?.targetKind);
340
- return zod4Scalar(config, visitor, node.value);
376
+ return zod4Scalar(config, visitor, node.value, schemaVisitor);
341
377
  }
342
378
  }
343
379
  function maybeLazy(visitor, type, schema) {
@@ -348,7 +384,7 @@ function maybeLazy(visitor, type, schema) {
348
384
  const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType);
349
385
  return isComplexType ? `z.lazy(() => ${schema})` : schema;
350
386
  }
351
- function zod4Scalar(config, visitor, scalarName) {
387
+ function zod4Scalar(config, visitor, scalarName, schemaVisitor) {
352
388
  if (config.scalarSchemas?.[scalarName])
353
389
  return config.scalarSchemas[scalarName];
354
390
  const tsType = visitor.getScalarType(scalarName);
@@ -364,5 +400,6 @@ function zod4Scalar(config, visitor, scalarName) {
364
400
  return config.defaultScalarTypeSchema;
365
401
  }
366
402
  console.warn('unhandled scalar name:', scalarName);
403
+ schemaVisitor?.markAnySchemaUsed();
367
404
  return anySchema;
368
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davars/graphql-codegen-zod",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "GraphQL Code Generator plugin that generates Zod v4 validation schemas with transform support",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,13 +13,15 @@
13
13
  "import": {
14
14
  "types": "./dist/index.d.ts",
15
15
  "default": "./dist/index.js"
16
- }
16
+ },
17
+ "default": "./dist/index.js"
17
18
  },
18
19
  "./config": {
19
20
  "import": {
20
21
  "types": "./dist/transform_config.d.ts",
21
22
  "default": "./dist/transform_config.js"
22
- }
23
+ },
24
+ "default": "./dist/transform_config.js"
23
25
  }
24
26
  },
25
27
  "main": "dist/index.js",
@@ -32,6 +34,11 @@
32
34
  "LICENSE",
33
35
  "README.md"
34
36
  ],
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "test": "vitest run",
40
+ "prepublishOnly": "npm run build"
41
+ },
35
42
  "peerDependencies": {
36
43
  "graphql": "^16.0.0",
37
44
  "zod": "^4.0.0"
@@ -58,9 +65,5 @@
58
65
  "typescript": "5.9.3",
59
66
  "vitest": "^4.0.0",
60
67
  "zod": "4.3.6"
61
- },
62
- "scripts": {
63
- "build": "tsc -p tsconfig.build.json",
64
- "test": "vitest run"
65
68
  }
66
- }
69
+ }