Relations & Extra Schemas
Learn how to configure nested relation validation safely and how to declare custom, standalone schemas that aren't tied to any database model.
Handling Relations
By default, Prisma Guard excludes relation fields (e.g. items Item[] or author User) from generated schemas.
Why are relations opt-in?
- Prevents Circular Reference Crashes: Automatic deep relation parsing can create circular dependency issues at runtime.
- Maintains Clear Schema Boundaries: Relations are typically validated at different API layers; forcing validation of deeply-nested relational objects is usually undesirable.
Enabling Relations (@zod.include)
To opt-in and include a relation in your validation schema, simply annotate the relation field with /// @zod.include:
model Order {
id String @id
/// @zod.include
items Item[]
}
Generated Zod Schema:
// generated/zod/order.ts
import { z } from "zod";
import { ItemCreateSchema } from "./item";
export const OrderCreateSchema = z.object({
id: z.string().trim(),
items: z.array(z.lazy(() => ItemCreateSchema)),
});
Circular Relations
Prisma Guard automatically detects circular references between models and wraps them in z.lazy() to prevent runtime instantiation errors:
model Category {
id String @id
parent Category? @relation("parent")
/// @zod.include
children Category[] @relation("parent")
}
Generated Zod Schema:
// generated/zod/category.ts
import { z } from "zod";
export const CategoryCreateSchema = z.object({
id: z.string().trim(),
parent: z.lazy(() => CategoryCreateSchema).nullish().optional(),
children: z.array(z.lazy(() => CategoryCreateSchema)),
});
Extra Non-Prisma Schemas
You can define entirely new Zod schemas that are not associated with any database models. These schemas are written directly inside your .prisma files and are compiled into the generated TypeScript files.
There are three ways to declare extra schemas:
1. Shorthand (Config Decorator Lookup)
Reference a decorator from your prisma-guard.config.js by name. The schema name is automatically PascalCased.
Config:
import { defineConfig, v } from "@explita/prisma-guard";
export default defineConfig({
decorators: {
updateQueueItem: v.object({
id: v.string().min(1, v.var("(messages).required", "../../lib/messages")),
status: v.var("AppointmentStatus"),
}),
},
});
Prisma Schema:
/// @zod.include(updateQueueItem)
Generated output — UpdateQueueItemBaseSchema, UpdateQueueItemCreateSchema, etc., with auto-resolved enum imports and v.var import paths.
2. Named Reference
Control the output schema name explicitly:
/// @zod.include(UpdateQueueItem: updateQueueItem)
Same as Form 1, but you control the generated schema name (UpdateQueueItem) independently from the decorator key.
3. Inline Raw Zod Schema
Define the schema directly inside your .prisma comments using raw Zod code (not v):
/// @zod.include(UpdateQueueItem: z.object({
/// @zod id: z.string().min(1, messages.required),
/// @zod status: AppointmentStatusEnum,
/// @zod }))
[!IMPORTANT] Inline schemas must use raw Zod syntax (e.g.
z.object,z.string), not thevproxy. Thevproxy only works insideprisma-guard.config.jsbecause it is JavaScript that is evaluated and stringified at configuration load time. In.prismacomments, the text is parsed as-is.
Smart Features
- Enum resolution: References like
v.var("AppointmentStatus")in configuration decorators are automatically matched against your Prisma enum definitions, appended with your configured suffix (e.g.,AppointmentStatusEnum), and auto-imported. - Import generation: Mappings like
v.var("(messages).required", "../../lib/messages")generateimport { messages } from "../../lib/messages";at the top of the compiled file using Set-based deduplication. - Relation support:
v.relation("Child")(orz.relation("Child")inside inline blocks) automatically resolves to circular-safez.lazy(() => ChildCreateSchema)calls with proper imports. - Full schema variants: Every declared extra schema generates
BaseSchema,CreateSchema, partialUpdateSchemavariants, scalar schemas, and fully typed inferred outputs—mirroring database models.
🚀 Quick Reference: @zod.include
| Requirement | Syntax |
|---|---|
| Reference config decorator | /// @zod.include(updateQueueItem) |
| Name + config decorator | /// @zod.include(UpdateQueueItem: updateQueueItem) |
| Inline schema | /// @zod.include(Name: z.object({...})) |
[!TIP] Which form should I use?
- Shorthand: When you already have a decorator defined in configuration.
- Named Reference: When the decorator key differs from the desired PascalCased schema output name.
- Inline Raw Zod: When the schema is a one-off or requires custom validation.
Persistence & Constants (v.var)
Prisma Guard generates a lib/constants.ts file in your output directory.
- Persistence: Unlike the
zod/folder (which is cleared and fully rebuilt every time), thelib/folder is persistent and will never be overwritten. - Version Control: The
metadata --vscodecommand automatically addszod/to your.gitignore, but it leaves thelib/folder alone. You should commit thelib/folder to your repository so your team shares the same validation constants. - Custom Messages: You can edit the
REQUIRED_MESSAGE(or any other custom variables) insidelib/constants.tsto suit your requirements. - The
v.var()Helper: Reference external variables and optionally let the compiler handle TS imports automatically.
Mappings Reference
| Syntax | Behavior | Generated Output |
|---|---|---|
v.var("constants.MSG") | Auto-import from lib/constants.ts | import { MSG } from "../lib/constants" |
v.var("(constants).VAL") | Ignore (No auto-import) | constants.VAL (assumes manual import) |
v.var("myVar") | Raw Reference (No auto-import) | myVar (assumes manual import) |
v.var("err", "../../lib/err") | Auto-import from custom relative path | import { err } from "../../lib/err" |
- Ignored References: Wrap a segment in parentheses (e.g.
v.var("(constants).genders")) to suppress the auto-import engine. This is particularly useful if you have manually imported the variable using the/// @zod.importdirective.
Configuration Example
// prisma-guard.config.js
import { v } from "@explita/prisma-guard";
export default {
decorators: {
// This will auto-import { EMAIL_MESSAGE } from "../lib/constants"
// and generate: .email(EMAIL_MESSAGE)
email: v.string().email(v.var("constants.EMAIL_MESSAGE")),
// This will NOT auto-import anything (useful if you have manual imports)
// and generate: .enum(constants.genders)
gender: v.enum(v.var("(constants).genders")),
// This will auto-import { OrderItemCreateSchema } from the correct file
// and generate: z.lazy(() => OrderItemCreateSchema)
orderItems: v.array(v.relation("OrderItem")),
},
};