diff --git a/example/myzod/schemas.ts b/example/myzod/schemas.ts index 3a47f823..50bfda38 100644 --- a/example/myzod/schemas.ts +++ b/example/myzod/schemas.ts @@ -1,5 +1,5 @@ import * as myzod from 'myzod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, Comment, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' export const definedNonNullAnySchema = myzod.object({}); @@ -25,6 +25,13 @@ export function AttributeInputSchema(): myzod.Type { }) } +export function CommentSchema(): myzod.Type { + return myzod.object({ + __typename: myzod.literal('Comment').optional(), + replies: myzod.array(myzod.lazy(() => CommentSchema())).optional().nullable() + }) +} + export function ComponentInputSchema(): myzod.Type { return myzod.object({ child: myzod.lazy(() => ComponentInputSchema().optional().nullable()), diff --git a/example/test.graphql b/example/test.graphql index eb3a699e..0322defd 100644 --- a/example/test.graphql +++ b/example/test.graphql @@ -121,3 +121,7 @@ directive @constraint( multipleOf: Float uniqueTypeName: String ) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + +type Comment { + replies: [Comment!] +} diff --git a/example/types.ts b/example/types.ts index d1c67290..9c0aec8b 100644 --- a/example/types.ts +++ b/example/types.ts @@ -31,6 +31,11 @@ export enum ButtonComponentType { Submit = 'SUBMIT' } +export type Comment = { + __typename?: 'Comment'; + replies?: Maybe>; +}; + export type ComponentInput = { child?: InputMaybe; childrens?: InputMaybe>>; diff --git a/example/valibot/schemas.ts b/example/valibot/schemas.ts index dbc09340..464f3eb9 100644 --- a/example/valibot/schemas.ts +++ b/example/valibot/schemas.ts @@ -1,5 +1,5 @@ import * as v from 'valibot' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, Comment, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' export const ButtonComponentTypeSchema = v.enum_(ButtonComponentType); @@ -23,6 +23,13 @@ export function AttributeInputSchema(): v.GenericSchema { }) } +export function CommentSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Comment')), + replies: v.nullish(v.array(v.lazy(() => CommentSchema()))) + }) +} + export function ComponentInputSchema(): v.GenericSchema { return v.object({ child: v.lazy(() => v.nullish(ComponentInputSchema())), diff --git a/example/yup/schemas.ts b/example/yup/schemas.ts index d78a3abd..ea99660a 100644 --- a/example/yup/schemas.ts +++ b/example/yup/schemas.ts @@ -1,5 +1,5 @@ import * as yup from 'yup' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User, UserKind } from '../types' +import { Admin, AttributeInput, ButtonComponentType, Comment, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User, UserKind } from '../types' export const ButtonComponentTypeSchema = yup.string().oneOf(Object.values(ButtonComponentType)).defined(); @@ -29,6 +29,13 @@ export function AttributeInputSchema(): yup.ObjectSchema { }) } +export function CommentSchema(): yup.ObjectSchema { + return yup.object({ + __typename: yup.string<'Comment'>().optional(), + replies: yup.array(yup.lazy(() => CommentSchema().nonNullable())).defined().nullable().optional() + }) +} + export function ComponentInputSchema(): yup.ObjectSchema { return yup.object({ child: yup.lazy(() => ComponentInputSchema()).optional(), diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts index 1cb9d68f..72e3a47a 100644 --- a/example/zod/schemas.ts +++ b/example/zod/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod/v3' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, Comment, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' type Properties = Required<{ [K in keyof T]: z.ZodType; @@ -33,6 +33,13 @@ export function AttributeInputSchema(): z.ZodObject> }) } +export function CommentSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Comment').optional(), + replies: z.array(z.lazy(() => CommentSchema())).nullish() + }) +} + export function ComponentInputSchema(): z.ZodObject> { return z.object({ child: z.lazy(() => ComponentInputSchema().nullish()), diff --git a/example/zodv4/schemas.ts b/example/zodv4/schemas.ts index 237ae4d0..3b8b3023 100644 --- a/example/zodv4/schemas.ts +++ b/example/zodv4/schemas.ts @@ -1,5 +1,5 @@ import * as z from 'zod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, Comment, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' type Properties = Required<{ [K in keyof T]: z.ZodType; @@ -33,6 +33,13 @@ export function AttributeInputSchema(): z.ZodObject> }) } +export function CommentSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Comment').optional(), + replies: z.array(z.lazy(() => CommentSchema())).nullish() + }) +} + export function ComponentInputSchema(): z.ZodObject> { return z.object({ child: z.lazy(() => ComponentInputSchema().nullish()), @@ -128,7 +135,7 @@ export function UserSchema(): z.ZodObject> { createdAt: definedNonNullAnySchema.nullish(), email: z.string().nullish(), id: z.string().nullish(), - kind: UserKindSchema().nullish(), + kind: z.lazy(() => UserKindSchema().nullish()), name: z.string().nullish(), password: z.string().nullish(), updatedAt: definedNonNullAnySchema.nullish() diff --git a/src/zodv4/index.ts b/src/zodv4/index.ts index d74c4cb0..38e1aabe 100644 --- a/src/zodv4/index.ts +++ b/src/zodv4/index.ts @@ -16,13 +16,14 @@ import type { Visitor } from '../visitor.js'; import { resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers'; import { convertNameParts, DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; import { + isEnumType, + isScalarType, Kind, } from 'graphql'; import { buildApi, formatDirectiveConfig } from '../directive.js'; import { escapeGraphQLCharacters, InterfaceTypeDefinitionBuilder, - isInput, isListType, isNamedType, isNonNullType, @@ -275,22 +276,22 @@ export class ZodV4SchemaVisitor extends BaseSchemaVisitor { function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { const gen = generateFieldTypeZodSchema(config, visitor, field, field.type); - return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); + return indent(`${field.name.value}: ${maybeLazy(visitor, field.type, gen)}`, indentCount); } function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { if (isListType(type)) { const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); if (!isNonNullType(parentType)) { - const arrayGen = `z.array(${maybeLazy(type.type, gen)})`; + const arrayGen = `z.array(${maybeLazy(visitor, type.type, gen)})`; const maybeLazyGen = applyDirectives(config, field, arrayGen); return `${maybeLazyGen}.nullish()`; } - return `z.array(${maybeLazy(type.type, gen)})`; + return `z.array(${maybeLazy(visitor, type.type, gen)})`; } if (isNonNullType(type)) { const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type); - return maybeLazy(type.type, gen); + return maybeLazy(visitor, type.type, gen); } if (isNamedType(type)) { const gen = generateNameNodeZodSchema(config, visitor, type.name); @@ -371,11 +372,14 @@ function generateNameNodeZodSchema(config: ValidationSchemaPluginConfig, visitor } } -function maybeLazy(type: TypeNode, schema: string): string { - if (isNamedType(type) && isInput(type.name.value)) - return `z.lazy(() => ${schema})`; +function maybeLazy(visitor: Visitor, type: TypeNode, schema: string): string { + if (!isNamedType(type)) { + return schema; + } - return schema; + const schemaType = visitor.getType(type.name.value); + const isComplexType = !isScalarType(schemaType) && !isEnumType(schemaType); + return isComplexType ? `z.lazy(() => ${schema})` : schema; } function zod4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { diff --git a/tests/zodv4.spec.ts b/tests/zodv4.spec.ts index 2cc2a0c4..9b784890 100644 --- a/tests/zodv4.spec.ts +++ b/tests/zodv4.spec.ts @@ -1336,7 +1336,7 @@ describe('zodv4', () => { export function BookSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Book').optional(), - author: AuthorSchema().nullish(), + author: z.lazy(() => AuthorSchema().nullish()), title: z.string().nullish() }) } @@ -1344,7 +1344,7 @@ describe('zodv4', () => { export function AuthorSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Author').optional(), - books: z.array(BookSchema().nullable()).nullish(), + books: z.array(z.lazy(() => BookSchema().nullable())).nullish(), name: z.string().nullish() }) } @@ -1621,7 +1621,7 @@ describe('zodv4', () => { export function GeometrySchema(): z.ZodObject> { return z.object({ __typename: z.literal('Geometry').optional(), - shape: ShapeSchema().nullish() + shape: z.lazy(() => ShapeSchema().nullish()) }) } " @@ -1772,7 +1772,7 @@ describe('zodv4', () => { export const GeometrySchema: z.ZodObject> = z.object({ __typename: z.literal('Geometry').optional(), - shape: ShapeSchema.nullish() + shape: z.lazy(() => ShapeSchema.nullish()) }); " `) @@ -1924,14 +1924,14 @@ describe('zodv4', () => { export function BookSchema(): z.ZodObject> { return z.object({ - author: AuthorSchema().nullish(), + author: z.lazy(() => AuthorSchema().nullish()), title: z.string().nullish() }) } export function AuthorSchema(): z.ZodObject> { return z.object({ - books: z.array(BookSchema().nullable()).nullish(), + books: z.array(z.lazy(() => BookSchema().nullable())).nullish(), name: z.string().nullish() }) } @@ -1987,7 +1987,7 @@ describe('zodv4', () => { export function BookSchema(): z.ZodObject> { return z.object({ title: z.string(), - author: AuthorSchema() + author: z.lazy(() => AuthorSchema()) }) } @@ -1995,7 +1995,7 @@ describe('zodv4', () => { return z.object({ __typename: z.literal('Textbook').optional(), title: z.string(), - author: AuthorSchema(), + author: z.lazy(() => AuthorSchema()), courses: z.array(z.string()) }) } @@ -2004,7 +2004,7 @@ describe('zodv4', () => { return z.object({ __typename: z.literal('ColoringBook').optional(), title: z.string(), - author: AuthorSchema(), + author: z.lazy(() => AuthorSchema()), colors: z.array(z.string()) }) } @@ -2012,7 +2012,7 @@ describe('zodv4', () => { export function AuthorSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Author').optional(), - books: z.array(BookSchema()).nullish(), + books: z.array(z.lazy(() => BookSchema())).nullish(), name: z.string().nullish() }) }