@@ -44,31 +44,43 @@ export class ZodV4SchemaVisitor extends BaseSchemaVisitor {
4444
4545 initialEmit ( ) : string {
4646 return (
47- `\n${
48- [
49- new DeclarationBlock ( { } )
50- . asKind ( 'type' )
51- . withName ( 'Properties<T>' )
52- . withContent ( [ 'Required<{' , ' [K in keyof T]: z.ZodType<T[K]>;' , '}>' ] . join ( '\n' ) )
53- . string ,
54- // Unfortunately, zod doesn’t provide non-null defined any schema.
55- // This is a temporary hack until it is fixed.
56- // see: https://github.com/colinhacks/zod/issues/884
57- new DeclarationBlock ( { } ) . asKind ( 'type' ) . withName ( 'definedNonNullAny' ) . withContent ( '{}' ) . string ,
58- new DeclarationBlock ( { } )
59- . export ( )
60- . asKind ( 'const' )
61- . withName ( `isDefinedNonNullAny` )
62- . withContent ( `(v: any): v is definedNonNullAny => v !== undefined && v !== null` )
63- . string ,
64- new DeclarationBlock ( { } )
65- . export ( )
66- . asKind ( 'const' )
67- . withName ( `${ anySchema } ` )
68- . withContent ( `z.any().refine((v) => isDefinedNonNullAny(v))` )
69- . string ,
70- ...this . enumDeclarations ,
71- ] . join ( '\n' ) } `
47+ `\n${ [
48+ new DeclarationBlock ( { } )
49+ . asKind ( 'type' )
50+ . withName ( 'Properties<T>' )
51+ . withContent ( [ 'Required<{' , ' [K in keyof T]: z.ZodType<T[K]>;' , '}>' ] . join ( '\n' ) )
52+ . string ,
53+ new DeclarationBlock ( { } )
54+ . asKind ( 'type' )
55+ . withName ( 'OneOf<T>' )
56+ . withContent ( [
57+ '{' ,
58+ ' [K in keyof Required<T>]: Required<{' ,
59+ ' [V in keyof Pick<Required<T>, K>]: z.ZodType<Pick<Required<T>, K>[V], unknown, any>' ,
60+ ' } & {' ,
61+ ' [P in Exclude<keyof T, K>]: z.ZodNever' ,
62+ ' }>' ,
63+ '}[keyof T]' ,
64+ ] . join ( '\n' ) )
65+ . string ,
66+ // Unfortunately, zod doesn’t provide non-null defined any schema.
67+ // This is a temporary hack until it is fixed.
68+ // see: https://github.com/colinhacks/zod/issues/884
69+ new DeclarationBlock ( { } ) . asKind ( 'type' ) . withName ( 'definedNonNullAny' ) . withContent ( '{}' ) . string ,
70+ new DeclarationBlock ( { } )
71+ . export ( )
72+ . asKind ( 'const' )
73+ . withName ( `isDefinedNonNullAny` )
74+ . withContent ( `(v: any): v is definedNonNullAny => v !== undefined && v !== null` )
75+ . string ,
76+ new DeclarationBlock ( { } )
77+ . export ( )
78+ . asKind ( 'const' )
79+ . withName ( `${ anySchema } ` )
80+ . withContent ( `z.any().refine((v) => isDefinedNonNullAny(v))` )
81+ . string ,
82+ ...this . enumDeclarations ,
83+ ] . join ( '\n' ) } `
7284 ) ;
7385 }
7486
@@ -78,6 +90,13 @@ export class ZodV4SchemaVisitor extends BaseSchemaVisitor {
7890 const visitor = this . createVisitor ( 'input' ) ;
7991 const name = visitor . convertName ( node . name . value ) ;
8092 this . importTypes . push ( name ) ;
93+
94+ const hasOneOf = node . directives ?. some ( directive => directive ?. name . value === 'oneOf' ) ;
95+ if ( hasOneOf ) {
96+ const result = this . buildOneOfInputFields ( node . fields ?? [ ] , visitor , name )
97+ return result ;
98+ }
99+
81100 return this . buildInputFields ( node . fields ?? [ ] , visitor , name ) ;
82101 } ,
83102 } ;
@@ -272,13 +291,89 @@ export class ZodV4SchemaVisitor extends BaseSchemaVisitor {
272291 . string ;
273292 }
274293 }
294+
295+ protected buildOneOfInputFields (
296+ fields : readonly ( FieldDefinitionNode | InputValueDefinitionNode ) [ ] ,
297+ visitor : Visitor ,
298+ name : string ,
299+ ) {
300+ // FIXME: If I end up adding an explicit z.ZodUnion return type, make sure to handle schemaNamespacedImportName
301+ // Also type prefix and suffix
302+
303+ const typeName = visitor . prefixTypeNamespace ( name ) ;
304+ const union = generateFieldUnionZodSchema ( this . config , visitor , fields , 2 )
305+
306+ switch ( this . config . validationSchemaExportType ) {
307+ case 'const' :
308+ return new DeclarationBlock ( { } )
309+ . export ( )
310+ . asKind ( 'const' )
311+ . withName ( `${ typeName } Schema` )
312+ . withContent ( [ 'z.union([' , union , '])' ] . join ( '\n' ) )
313+ . string ;
314+
315+ case 'function' :
316+ default :
317+ return new DeclarationBlock ( { } )
318+ . export ( )
319+ . asKind ( 'function' )
320+ . withName ( `${ name } Schema(): z.ZodUnion<z.ZodObject<OneOf<${ typeName } >>[]>` )
321+ . withBlock ( [ indent ( `return z.union([` ) , `${ union } ` , indent ( '])' ) ] . join ( '\n' ) )
322+ . string ;
323+ }
324+ }
275325}
276326
277327function generateFieldZodSchema ( config : ValidationSchemaPluginConfig , visitor : Visitor , field : InputValueDefinitionNode | FieldDefinitionNode , indentCount : number ) : string {
278328 const gen = generateFieldTypeZodSchema ( config , visitor , field , field . type ) ;
279329 return indent ( `${ field . name . value } : ${ maybeLazy ( visitor , field . type , gen ) } ` , indentCount ) ;
280330}
281331
332+ function generateFieldUnionZodSchema ( config : ValidationSchemaPluginConfig , visitor : Visitor , fields : readonly ( FieldDefinitionNode | InputValueDefinitionNode ) [ ] , indentCount : number ) : string {
333+ const union = fields . map ( ( field ) => {
334+ const objectFields = fields . map ( ( nestedObjectField ) => {
335+ const isSameField = field . name === nestedObjectField . name ;
336+
337+ if ( ! isSameField ) {
338+ return indent ( `${ nestedObjectField . name . value } : z.never()` , indentCount + 1 ) ;
339+ }
340+
341+ if ( isListType ( nestedObjectField . type ) ) {
342+ const gen = generateFieldTypeZodSchema ( config , visitor , nestedObjectField , nestedObjectField . type . type , nestedObjectField . type ) ;
343+
344+ const maybeLazyGen = `z.array(${ maybeLazy ( visitor , nestedObjectField . type . type , gen ) } )` ;
345+ const appliedDirectivesGen = applyDirectives ( config , nestedObjectField , maybeLazyGen ) ;
346+
347+ return indent ( `${ nestedObjectField . name . value } : ${ appliedDirectivesGen } ` , 3 ) ;
348+ }
349+
350+ if ( isNonNullType ( nestedObjectField . type ) ) {
351+ const gen = generateFieldTypeZodSchema ( config , visitor , field , nestedObjectField . type . type , nestedObjectField . type ) ;
352+
353+ return indent ( `${ nestedObjectField . name . value } : ${ maybeLazy ( visitor , nestedObjectField . type . type , gen ) } ` , indentCount + 1 ) ;
354+ }
355+
356+ if ( isNamedType ( nestedObjectField . type ) ) {
357+ const gen = generateNameNodeZodSchema ( config , visitor , nestedObjectField . type . name ) ;
358+
359+ const appliedDirectivesGen = applyDirectives ( config , nestedObjectField , gen ) ;
360+
361+ if ( visitor . shouldEmitAsNotAllowEmptyString ( nestedObjectField . type . name . value ) )
362+ return indent ( `${ nestedObjectField . name . value } : ${ maybeLazy ( visitor , nestedObjectField . type , appliedDirectivesGen ) } .min(1)` , indentCount + 1 ) ;
363+
364+ return indent ( `${ nestedObjectField . name . value } : ${ maybeLazy ( visitor , nestedObjectField . type , appliedDirectivesGen ) } ` , indentCount + 1 ) ;
365+ }
366+
367+ console . warn ( 'unhandled type:' , field . type ) ;
368+ return '' ;
369+ } ) ;
370+
371+ return [ indent ( 'z.object({' , indentCount ) , objectFields . join ( ',\n' ) , indent ( '})' , indentCount ) ] . join ( '\n' ) ;
372+ } ) . join ( ',\n' ) ;
373+
374+ return union ;
375+ }
376+
282377function generateFieldTypeZodSchema ( config : ValidationSchemaPluginConfig , visitor : Visitor , field : InputValueDefinitionNode | FieldDefinitionNode , type : TypeNode , parentType ?: TypeNode ) : string {
283378 if ( isListType ( type ) ) {
284379 const gen = generateFieldTypeZodSchema ( config , visitor , field , type . type , type ) ;
0 commit comments