Context
I'm building a Next.js application with TypeScript and Supabase as my backend. Supabase CLI automatically generates TypeScript types from my database schema, which look like this:
export type Database = {
public: {
Tables: {
ingredients: {
Row: {
calories: number | null
carbohydrates: number | null
created_at: string
fiber: number | null
id: number
lipids: number | null
name: string
proteins: number | null
saturated_fat: number | null
sodium: number | null
sugars: number | null
}
Insert: {
calories?: number | null
carbohydrates?: number | null
created_at?: string
fiber?: number | null
id?: number
lipids?: number | null
name: string
proteins?: number | null
saturated_fat?: number | null
sodium?: number | null
sugars?: number | null
}
Update: {
calories?: number | null
carbohydrates?: number | null
created_at?: string
fiber?: number | null
id?: number
lipids?: number | null
name?: string
proteins?: number | null
saturated_fat?: number | null
sodium?: number | null
sugars?: number | null
}
}
// ... other tables
}
}
}
Problem
I want to use Zod to validate data in my API requests, but I don't want to manually maintain the schemas in sync with my Supabase types. I need a way to:
- Automatically generate Zod schemas from the Supabase types
- Keep the schemas in sync when the database schema changes
Current approach
Currently, I'm manually creating my Zod schemas like this:
export const ingredientSchema = z.object({
name: z.string().min(1, "Ingredient name is required"),
calories: z.number().nullable().optional(),
carbohydrates: z.number().nullable().optional(),
fiber: z.number().nullable().optional(),
lipids: z.number().nullable().optional(),
proteins: z.number().nullable().optional(),
saturated_fat: z.number().nullable().optional(),
sodium: z.number().nullable().optional(),
sugars: z.number().nullable().optional(),
});
But this approach is:
- Error-prone: I might miss fields when the database schema changes
- Redundant: I'm essentially duplicating type information
- Hard to maintain: Changes need to be made in multiple places
Question
What's the best way to automatically generate Zod schemas from Supabase-generated types while still allowing custom validation rules? Is there a utility or pattern that could help with this?
Any help or guidance would be greatly appreciated!
Context
I'm building a Next.js application with TypeScript and Supabase as my backend. Supabase CLI automatically generates TypeScript types from my database schema, which look like this:
export type Database = {
public: {
Tables: {
ingredients: {
Row: {
calories: number | null
carbohydrates: number | null
created_at: string
fiber: number | null
id: number
lipids: number | null
name: string
proteins: number | null
saturated_fat: number | null
sodium: number | null
sugars: number | null
}
Insert: {
calories?: number | null
carbohydrates?: number | null
created_at?: string
fiber?: number | null
id?: number
lipids?: number | null
name: string
proteins?: number | null
saturated_fat?: number | null
sodium?: number | null
sugars?: number | null
}
Update: {
calories?: number | null
carbohydrates?: number | null
created_at?: string
fiber?: number | null
id?: number
lipids?: number | null
name?: string
proteins?: number | null
saturated_fat?: number | null
sodium?: number | null
sugars?: number | null
}
}
// ... other tables
}
}
}
Problem
I want to use Zod to validate data in my API requests, but I don't want to manually maintain the schemas in sync with my Supabase types. I need a way to:
- Automatically generate Zod schemas from the Supabase types
- Keep the schemas in sync when the database schema changes
Current approach
Currently, I'm manually creating my Zod schemas like this:
export const ingredientSchema = z.object({
name: z.string().min(1, "Ingredient name is required"),
calories: z.number().nullable().optional(),
carbohydrates: z.number().nullable().optional(),
fiber: z.number().nullable().optional(),
lipids: z.number().nullable().optional(),
proteins: z.number().nullable().optional(),
saturated_fat: z.number().nullable().optional(),
sodium: z.number().nullable().optional(),
sugars: z.number().nullable().optional(),
});
But this approach is:
- Error-prone: I might miss fields when the database schema changes
- Redundant: I'm essentially duplicating type information
- Hard to maintain: Changes need to be made in multiple places
Question
What's the best way to automatically generate Zod schemas from Supabase-generated types while still allowing custom validation rules? Is there a utility or pattern that could help with this?
Any help or guidance would be greatly appreciated!
Share Improve this question edited Nov 20, 2024 at 23:36 Brian Tompsett - 汤莱恩 5,89372 gold badges61 silver badges133 bronze badges asked Nov 20, 2024 at 10:48 BlakyrisBlakyris 134 bronze badges 1- Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. – Community Bot Commented Nov 20, 2024 at 11:25
1 Answer
Reset to default 0This isn't ideal but has helped me and ensures that everything is typed which helps with the errors and maintenance. It was inspired by a thread I found when dealing with a similar challenge.
This is a helper utility to infer the Zod schema from the type. It means you get:
- typed schema creation
- auto-complete on available columns from a table
- the Zod type must match the Supabase column type to be valid
The utility:
import type { z } from "zod";
export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
type NonUndefined<T> = Exclude<T, undefined>;
// Infer the Zod schema from a TypeScript type to construct a typed Zod object.
export type ZodInferSchema<T extends object> = {
[Key in keyof T]-?: Equals<T[Key], NonUndefined<T[Key]>> extends false
? // biome-ignore lint/suspicious/noExplicitAny: Allowing any for flexibility
z.ZodOptional<z.ZodType<NonNullable<T[Key]>>> | z.ZodPipeline<z.ZodOptional<z.ZodType<any>>, z.ZodType<T[Key]>>
: // biome-ignore lint/suspicious/noExplicitAny: Allowing any for flexibility
z.ZodType<T[Key]> | z.ZodPipeline<z.ZodType<any>, z.ZodType<T[Key]>>;
};
Using this helper in your schema definition:
import type { Tables } from "@/lib/supabase/types";
import { z } from "zod";
import type { ZodInferSchema } from "@/lib/utils/functions/type-to-zod";
type Change = Tables<"change_log">;
const change = z.object<ZodInferSchema<Change>>({
changed_at: z.string(),
id: z.string(),
operation: z.string(),
record_id: z.string(),
site_id: z.string().nullable(),
table_name: z.string(),
});
export const selectChangesSchema = change;
export const insertChangeSchema = change
.extend({
changed_at: z.string().datetime(),
site_id: z.string(),
})
.partial({
id: true,
});
export type InsertChangeSchema = z.infer<typeof insertChangeSchema>;
I'd love to know if you find a better way!