diff --git a/src/zod/index.ts b/src/zod/index.ts index fc77e16d..40874baa 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -50,7 +50,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .withName('Properties') .withContent(['Required<{', ' [K in keyof T]: z.ZodType;', '}>'].join('\n')) .string, - // Unfortunately, zod doesn’t provide non-null defined any schema. + // Unfortunately, zod doesn't provide non-null defined any schema. // This is a temporary hack until it is fixed. // see: https://github.com/colinhacks/zod/issues/884 new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string, @@ -77,6 +77,12 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { const visitor = this.createVisitor('input'); const name = visitor.convertName(node.name.value); this.importTypes.push(name); + // Check for @oneOf directive + const hasOneOf = node.directives?.some(d => d.name.value === 'oneOf'); + if (hasOneOf) { + return this.buildOneOfInputFields(node.fields ?? [], visitor, name); + } + return this.buildInputFields(node.fields ?? [], visitor, name); }, }; @@ -274,6 +280,52 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .string; } } + + protected buildOneOfInputFields( + fields: readonly InputValueDefinitionNode[], + visitor: Visitor, + name: string, + ): string { + // Generate discriminated union variants + const variants = fields.map((selectedField) => { + const fieldName = selectedField.name.value; + // Get the raw schema without nullish wrapper for discriminated union + const fieldSchema = generateFieldTypeZodSchema(this.config, visitor, selectedField, selectedField.type, { + kind: Kind.NON_NULL_TYPE, + type: { + kind: Kind.NAMED_TYPE, + name: selectedField.name, + } + }); + return indent(`z.object({\n`, 2) + + indent(` __type: z.literal("${fieldName}"),\n`, 2) + + indent(` ${fieldName}: ${fieldSchema}\n`, 2) + + indent(`})`, 2); + }).join(',\n'); + + switch (this.config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema`) + .withContent(`z.discriminatedUnion("__type", [\n ${variants}\n])`) + .string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodSchema<${name}>`) + .withBlock([ + indent(`return z.discriminatedUnion("__type", [`), + variants, + indent(`]);`), + ].join('\n')) + .string; + } + } } function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index b025ae04..720024f6 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -1797,4 +1797,54 @@ describe('zod', () => { " `) }); + + it('with @oneOf directive', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UpdateUserInput @oneOf { + email: String + phoneNumber: String + profile: UpdateUserProfileInput + } + + input UpdateUserProfileInput { + name: String + age: Int + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function UpdateUserInputSchema(): z.ZodSchema { + return z.discriminatedUnion("__type", [ + z.object({ + __type: z.literal("email"), + email: z.string() + }), + z.object({ + __type: z.literal("phoneNumber"), + phoneNumber: z.string() + }), + z.object({ + __type: z.literal("profile"), + profile: UpdateUserProfileInputSchema() + }) + ]); + } + + export function UpdateUserProfileInputSchema(): z.ZodObject> { + return z.object({ + name: z.string().nullish(), + age: z.number().nullish() + }) + } + " + `) + }); });