Skip to content

Commit b004868

Browse files
authored
fix: zod4 compatibility regarding fields with default values (#2233)
1 parent 8bbe219 commit b004868

File tree

3 files changed

+62
-37
lines changed

3 files changed

+62
-37
lines changed

packages/runtime/src/local-helpers/zod-utils.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { type ZodError } from 'zod';
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { fromZodError as fromZodErrorV3 } from 'zod-validation-error/v3';
33
import { fromZodError as fromZodErrorV4 } from 'zod-validation-error/v4';
4-
import { type ZodError as Zod4Error } from 'zod/v4';
54

65
/**
76
* Formats a Zod error message for better readability. Compatible with both Zod v3 and v4.
@@ -13,9 +12,9 @@ export function getZodErrorMessage(err: unknown): string {
1312

1413
try {
1514
if ('_zod' in err) {
16-
return fromZodErrorV4(err as Zod4Error).message;
15+
return fromZodErrorV4(err as any).message;
1716
} else {
18-
return fromZodErrorV3(err as ZodError).message;
17+
return fromZodErrorV3(err as any).message;
1918
}
2019
} catch {
2120
return err.message;

packages/schema/src/plugins/zod/generator.ts

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime';
2+
import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
23
import {
34
ExpressionContext,
45
PluginError,
@@ -23,10 +24,19 @@ import {
2324
resolvePath,
2425
saveSourceFile,
2526
} from '@zenstackhq/sdk';
26-
import { DataModel, EnumField, Model, TypeDef, isArrayExpr, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
27+
import {
28+
DataModel,
29+
DataModelField,
30+
EnumField,
31+
Model,
32+
TypeDef,
33+
isArrayExpr,
34+
isDataModel,
35+
isEnum,
36+
isTypeDef,
37+
} from '@zenstackhq/sdk/ast';
2738
import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers';
2839
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
29-
import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
3040
import { streamAllContents } from 'langium';
3141
import path from 'path';
3242
import type { CodeBlockWriter, SourceFile } from 'ts-morph';
@@ -418,25 +428,10 @@ export const ${typeDef.name}Schema = ${refineFuncName}(${noRefineSchema});
418428
this.addPreludeAndImports(model, writer, output);
419429

420430
// base schema - including all scalar fields, with optionality following the schema
421-
writer.write(`const baseSchema = z.object(`);
422-
writer.inlineBlock(() => {
423-
scalarFields.forEach((field) => {
424-
writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`);
425-
});
426-
});
431+
this.createModelBaseSchema('baseSchema', writer, scalarFields, true);
427432

428-
switch (this.options.mode) {
429-
case 'strip':
430-
// zod strips by default
431-
writer.writeLine(')');
432-
break;
433-
case 'passthrough':
434-
writer.writeLine(').passthrough();');
435-
break;
436-
default:
437-
writer.writeLine(').strict();');
438-
break;
439-
}
433+
// base schema without field defaults
434+
this.createModelBaseSchema('baseSchemaWithoutDefaults', writer, scalarFields, false);
440435

441436
// relation fields
442437

@@ -536,7 +531,9 @@ export const ${upperCaseFirst(model.name)}Schema = ${modelSchema};
536531
////////////////////////////////////////////////
537532

538533
// schema for validating prisma create input (all fields optional)
539-
let prismaCreateSchema = this.makePassthrough(this.makePartial(`baseSchema${omitDiscriminators}`));
534+
let prismaCreateSchema = this.makePassthrough(
535+
this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`)
536+
);
540537
if (refineFuncName) {
541538
prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`;
542539
}
@@ -554,7 +551,7 @@ export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${prismaCreateSch
554551
${scalarFields
555552
.filter((f) => !isDiscriminatorField(f))
556553
.map((field) => {
557-
let fieldSchema = makeFieldSchema(field);
554+
let fieldSchema = makeFieldSchema(field, false);
558555
if (field.type.type === 'Int' || field.type.type === 'Float') {
559556
fieldSchema = `z.union([${fieldSchema}, z.record(z.unknown())])`;
560557
}
@@ -577,7 +574,7 @@ export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSch
577574
// 3. Create schema
578575
////////////////////////////////////////////////
579576

580-
let createSchema = `baseSchema${omitDiscriminators}`;
577+
let createSchema = `baseSchemaWithoutDefaults${omitDiscriminators}`;
581578
const fieldsWithDefault = scalarFields.filter(
582579
(field) => hasAttribute(field, '@default') || hasAttribute(field, '@updatedAt') || field.type.array
583580
);
@@ -631,7 +628,7 @@ export const ${upperCaseFirst(model.name)}CreateSchema = ${createSchema};
631628
////////////////////////////////////////////////
632629

633630
// for update all fields are optional
634-
let updateSchema = this.makePartial(`baseSchema${omitDiscriminators}`);
631+
let updateSchema = this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`);
635632

636633
// export schema with only scalar fields: `[Model]UpdateScalarSchema`
637634
const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`;
@@ -673,6 +670,33 @@ export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema};
673670
return schemaName;
674671
}
675672

673+
private createModelBaseSchema(
674+
name: string,
675+
writer: CodeBlockWriter,
676+
scalarFields: DataModelField[],
677+
addDefaults: boolean
678+
) {
679+
writer.write(`const ${name} = z.object(`);
680+
writer.inlineBlock(() => {
681+
scalarFields.forEach((field) => {
682+
writer.writeLine(`${field.name}: ${makeFieldSchema(field, addDefaults)},`);
683+
});
684+
});
685+
686+
switch (this.options.mode) {
687+
case 'strip':
688+
// zod strips by default
689+
writer.writeLine(')');
690+
break;
691+
case 'passthrough':
692+
writer.writeLine(').passthrough();');
693+
break;
694+
default:
695+
writer.writeLine(').strict();');
696+
break;
697+
}
698+
}
699+
676700
private createRefineFunction(decl: DataModel | TypeDef, writer: CodeBlockWriter) {
677701
const refinements = this.makeValidationRefinements(decl);
678702
let refineFuncName: string | undefined;

packages/schema/src/plugins/zod/utils/schema-gen.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers';
1515
import { isDefaultWithAuth } from '../../enhancer/enhancer-utils';
1616

17-
export function makeFieldSchema(field: DataModelField | TypeDefField) {
17+
export function makeFieldSchema(field: DataModelField | TypeDefField, addDefaults: boolean = true) {
1818
if (isDataModel(field.type.reference?.ref)) {
1919
if (field.type.array) {
2020
// array field is always optional
@@ -141,14 +141,16 @@ export function makeFieldSchema(field: DataModelField | TypeDefField) {
141141
schema += '.optional()';
142142
}
143143
} else {
144-
const schemaDefault = getFieldSchemaDefault(field);
145-
if (schemaDefault !== undefined) {
146-
if (field.type.type === 'BigInt') {
147-
// we can't use the `n` BigInt literal notation, since it needs
148-
// ES2020 or later, which TypeScript doesn't use by default
149-
schema += `.default(BigInt("${schemaDefault}"))`;
150-
} else {
151-
schema += `.default(${schemaDefault})`;
144+
if (addDefaults) {
145+
const schemaDefault = getFieldSchemaDefault(field);
146+
if (schemaDefault !== undefined) {
147+
if (field.type.type === 'BigInt') {
148+
// we can't use the `n` BigInt literal notation, since it needs
149+
// ES2020 or later, which TypeScript doesn't use by default
150+
schema += `.default(BigInt("${schemaDefault}"))`;
151+
} else {
152+
schema += `.default(${schemaDefault})`;
153+
}
152154
}
153155
}
154156

0 commit comments

Comments
 (0)