Skip to content

Commit 632607c

Browse files
committed
feat(zodv4): oneOf schema handling
1 parent f116314 commit 632607c

File tree

2 files changed

+987
-466
lines changed

2 files changed

+987
-466
lines changed

src/zodv4/index.ts

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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

277327
function 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+
282377
function 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

Comments
 (0)