Skip to main content

Default Values

A Prisma @default(...) becomes a Zod .default(...) in pure model schemas only. Every other artifact leaves the field without one: CRUD and object input schemas express a database default as relaxed requiredness instead (a field with a default is .optional() there), and variant files never emit a default.

ArtifactCarries .default(...) from @default(...)
models/<Model>.schema.ts (pureModels: true)Yes
The pure-model section of a single-file bundleYes
objects/* and CRUD operation schemasNo — the field becomes optional instead
results/*No for the model's own scalars (relation fields reference pure model schemas, which do carry them)
variants/pure, variants/input, variants/resultNo
Array-based custom variant filesNo

So defaults are only visible once pure models are on:

zod-generator.config.json
{
"pureModels": true
}

mode: "minimal" enables pureModels for you. See Generation Modes and Pure Model Schemas.

Literal defaults

The literal is re-emitted in the base schema's own runtime type, so the default is a valid input for the schema it is attached to. The last column lists the only options that change the emitted text.

Prisma type@default(...)Emitted in the pure modelConfig that changes it
String@default("light")z.string().default("light")
Int@default(10)z.number().int().default(10)
Float@default(30.0)z.number().default(30.0)
Boolean@default(true)z.boolean().default(true)
BigInt@default(0)z.bigint().default(BigInt("0"))jsonSchemaCompatible, jsonSchemaOptions.bigIntFormat
DateTime@default("2020-01-01T00:00:00.000Z")z.date().default(new Date("2020-01-01T00:00:00.000Z"))dateTimeStrategy, jsonSchemaCompatible
Decimal@default(1).default(new Prisma.Decimal(1)) appended to the z.custom<InstanceType<typeof Prisma.Decimal>>(...) basedecimalMode
Json@default("{}").default({}) appended after the nesting-depth refinement
Bytes@default("SGVsbG8=")z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Must be valid base64 string").max(22369622, "Base64 string too long").default("SGVsbG8=")jsonSchemaCompatible, jsonSchemaOptions.bytesFormat
enum@default(USER)RoleSchema.default("USER")naming.enum.exportNamePattern (changes the export name only)

Float keeps a trailing .0: a Prisma @default(30.0) arrives from DMMF as the number 30, and the generator re-formats it as 30.0 so the emitted text still reads as a float.

Function defaults

Only now(), uuid() and cuid() are recognised by name, and none of them produces a .default(...) in the output. Every other function default — autoincrement(), dbgenerated(...), auto(), nanoid(), sequence() and so on — is likewise emitted without a default.

Prisma defaultEmitted in the pure modelWhy
@default(now())no .default(...) — for example createdAt: z.date()a DateTime with a now() default is classified as auto-generated, and that classification replaces the field's modifier chain
@default(uuid())no .default(...)z.string()no inline UUID generator is emitted, so the schema needs no extra imports; the database supplies the value
@default(cuid())no .default(...)z.string()same: no inline CUID helper is emitted
@default(autoincrement())no .default(...)z.number().int()the value is assigned by the database
@default(dbgenerated("gen_random_uuid()"))no .default(...)the expression is database-side and has no JavaScript equivalent

Three more cases produce no default, whatever the value:

  • Any @id field with a default. An id field that has any default — @default(autoincrement()), @default(uuid()), or even a literal @default(5) — is treated as auto-generated, and the literal is dropped along with it.
  • @updatedAt fields, which are auto-generated by definition.
  • List defaults. String[] @default(["a", "b"]) emits z.array(z.string()) with no default.

A worked example

schema.prisma
model Settings {
id Int @id @default(autoincrement())
theme String @default("light")
maxItems Int @default(10)
timeout Float @default(30.0)
isActive Boolean @default(true)
count BigInt @default(0)
expiresAt DateTime @default("2020-01-01T00:00:00.000Z")
createdAt DateTime @default(now())
}

With pureModels: true and otherwise stock options:

generated/schemas/models/Settings.schema.ts
export const SettingsSchema = z.object({
id: z.number().int(),
theme: z.string().default("light"),
maxItems: z.number().int().default(10),
timeout: z.number().default(30.0),
isActive: z.boolean().default(true),
count: z.bigint().default(BigInt("0")),
expiresAt: z.date().default(new Date("2020-01-01T00:00:00.000Z")),
createdAt: z.date(),
});

Where .default(...) sits in the chain

Two ordering rules apply, and both matter for whether the file compiles:

  1. After the validation chain. Validators such as .regex() and .max() exist on the base type, not on ZodDefault, so z.string().default("...").regex(...) throws at import time. Any trailing .default(...) is therefore moved behind the whole chain, which is what makes the Bytes row above end in .default("SGVsbG8=").
  2. Before the optionality wrapper. An optional field with a default emits z.bigint().default(BigInt("1")).nullish() — the wrapper (.nullish() by default) is appended last. See Optional Field Behavior.

The same holds for @zod validations, since those are also part of the base chain: String @default("abc") with /// @zod.min(2).max(10) emits z.string().min(2).max(10).default("abc").

Per-type notes

BigInt

BigInt defaults are emitted through the string-argument constructor, BigInt("0"), rather than a 0n literal. A bigint literal requires an ES2020-or-later TypeScript target, so the constructor form keeps the generated file compiling whatever target the consuming project uses. It also stays exact past Number.MAX_SAFE_INTEGER: @default(9007199254740993) emits BigInt("9007199254740993").

DateTime

A literal timestamp becomes new Date("...") under dateTimeStrategy: "date" (the default) and "coerce", because the base schema is z.date() / z.coerce.date(). Under dateTimeStrategy: "isoString" the base is a validated string that transforms into a Date, so the default stays the raw ISO string:

expiresAt: z.string().regex(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/, "Invalid ISO datetime").transform(v => new Date(v)).default("2020-01-01T00:00:00.000Z"),

The text inside new Date(...) is whatever DMMF reports for the literal, so the offset spelling can differ between Prisma versions (...T00:00:00.000Z or ...T00:00:00+00:00). See DateTime Strategy.

Decimal

In decimalMode: "decimal" (the default) the literal is wrapped: .default(new Prisma.Decimal(1)). This is one of the reasons Prisma is imported as a value rather than as a type in that mode. "number" and "string" mode emit the literal unwrapped, exactly as DMMF reports it — so Decimal @default(1) under decimalMode: "string" emits .default(1) against a z.string() base, which does not type-check; prefer "decimal" or "number" for Decimal fields that carry a default. See Special Type Mapping for the Decimal bases themselves.

Json

A Json default arrives as text. If that text parses as JSON it is inlined as the parsed literal, so @default("{}") becomes .default({}) and @default("[]") becomes .default([]); text that does not parse keeps its quoted form. The default is appended after the depth refinement that pure models always apply to Json:

meta: z.unknown().refine((val) => { const getDepth = (obj: unknown, depth: number = 0): number => { if (depth > 10) return depth; if (obj === null || typeof obj !== 'object') return depth; const values = Object.values(obj as Record<string, unknown>); if (values.length === 0) return depth; return Math.max(...values.map(v => getDepth(v, depth + 1))); }; return getDepth(val) <= 10; }, "JSON nesting depth exceeds maximum of 10").default({}),

Bytes

Pure models represent Bytes as a base64 z.string(), so a Bytes default is emitted as the base64 string it already is, appended after the base64 regex and the .max() derived from the internal 16 MB ceiling. The generator's internal Uint8Array representation decodes the same default lazily and browser-safely instead:

blob: z.instanceof(Uint8Array).default(() => Uint8Array.from(atob("SGVsbG8="), (c) => c.charCodeAt(0))),

That form is not reachable from a config file — there is no option to switch pure-model Bytes to Uint8Array. See Bytes & JSON Details.

jsonSchemaCompatible mode

With jsonSchemaCompatible: true, BigInt and DateTime bases become validated strings, so their defaults stay raw string literals instead of being wrapped in a constructor:

count: z.string().regex(/^\d+$/, "Invalid bigint string").default("0"),
expiresAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, "Invalid ISO datetime").default("2020-01-01T00:00:00.000Z"),

Setting jsonSchemaOptions.bigIntFormat: "number" switches the base to z.number().int() and the default to .default(0). decimalMode is unaffected by this flag, so a Decimal default is still wrapped in new Prisma.Decimal(...). See JSON Schema Compatibility.

Overriding a default

A @zod.default(...) annotation wins over the Prisma default. The annotation is applied to the base schema first, and the derived default is then dropped rather than emitted twice:

model Flags {
id Int @id @default(autoincrement())
/// @zod.default(true)
isActive Boolean @default(false)
}
isActive: z.boolean().default(true),

The annotation also works on fields with no Prisma default at all. See Zod Comment Annotations.