-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
How to use Zod schema with existing TypeScript interface? #2807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
In practice (assuming that you actually do something with your parsed data), wouldn't code like this be enough to do such a check? import z from 'zod';
interface Person {
name: string;
age?: number;
sex: "male" | "female" | "other" | null;
}
function doSomethingWithPerson(person: Person) {
return person;
}
const badSchema = z.object({
name: z.number(),
});
const personBad = badSchema.parse({ name: 'foo' });
doSomethingWithPerson(personBad);
// ❌ Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
// Property 'sex' is missing in type '{ name: number; }' but required in type 'Person'.(2345) This will throw an error if your schema does not match the interface. I'm not sure why you would need an additional assertion, but if you do, code like you suggested would be fine for that. |
I'm also wondering how this can be achieved. I have an existing interface I'd like to make sure that my zod schema always matches. It's easy enough to run my existing interface thru a zod schema generator and generate the matching zod schema but that does not ensure it stays in sync over time. If the original interface is updated, I want a compile time error telling me to update my zod schema. This is is possible with the io-ts library but I am struggling to figure out how to do it with zod. |
Adding my two cents to this. I also don't see any solution actually being given in issues such as #53. I don't want Zod to determine what my TypeScript types and interfaces look like, I want my TypeScript types and interfaces to determine what Zod looks like. I would like: interface Car {
make: string;
model: string;
} To lead to:
Not the other way around. As awesome as Zod is, and how eager I was to use it, I put more trust into TypeScript being around and maintained for a long time than I do in Zod. If for any reason I need to move away from Zod in the future, if all my types are inferred through Zod, that becomes a real issue. As eager as I was to use Zod on my current project, I will probably choose something else instead because of this design choice. |
I think because TS is a superset of JS and an interface doesn't actually exist in JS-land, there's nothing tangible from an interface come transpile/run time. Ultimately there would need to be something that generated code at build, or even a code generator before build/run time that would take TS interfaces and build out the associated code validation (ie: Zod). Maybe there's some type of reflection that could be utilized to help do this? Some interesting links around such: |
I think the prefered way of using it / the perfect solution would be something like: //simple interface or type with any properties, most likely generated
interface Human {
name: string;
age?: number;
}
// to any zod-schema:
const schema = z.from<Human>();
// or even something like this would be OK:
class Person implements Human {
// ... constructor e
8000
tc
}
const another = z.create(Person);
// you would parse as normal
const unknownData = {propA: "a", name: "john", age: "123"}
const parsed = schema.parse(unknownData);
// ^? = typeof Human
// same for .safeParse and safeParseAsync Would that even be possible to extract properties and types from an interface? |
This is kind of backwards and not at all the goal of zod. It is not possible to create runtime-code from TS without additional build steps. Zod is a JS library, not a build tool. But someone else could create such a "ts-to-zod" bundler plugin, which could use typescript AST parsing to generate new JS code at build time. However, then your TS types become the source of truth, and those are less expressive than what zod provides. /edit: just saw that someone built this: https://github.com/fabien0102/ts-to-zod |
Let's say you have a database and an ORM to talk to it, that ORM has types but no validation. I think this could be done during runtime, how I am not sure. |
I'd like to add to this that it's not just about trust but also about source of truth being generated by other tools. Today many types get generated from for instance |
This is still boilerplatey, but what about something like this? interface Car {
make: string;
model: string;
}
const Car = z.object({
make: z.string(),
model: z.string()
}) satisfies ZodType<Car> This guarantees that the zod schema you define is in sync with the interface that already exists. I've used this approach a few times when the type I'm modeling comes from some other space I don't control (codegen, upstream libraries, etc.). |
It does, but at the same time you have to redefine your properties that are already set in the interface, but for zod. |
Yeah, as @graup mentioned, I don't think that's a thing zod is going to handle itself; to generate zod parsers from static types you'll need codegen tools like ts-to-zod. My experience is that codegen tools tend to be painful to work with and lead to bad dev experiences, so I tend to opt for the statically checked repetition instead. That may be because in my cases it's usually not properties that I've already set; if I'm doing this, it's because I'm trying to create a zod parser for a type I don't control. If I did control it, I'd just derive the type from the zod parser. |
I've used the import * as Joi from 'joi'
interface Car {
make: string;
model: string;
}
const Car = Joi.object<Car>({
make: Joi.string(),
model: Joi.string()
}) Truth be told, it doesn't support the inverse (what So, if you need your schema validation library to support defining a schema based on your TS type, you can look for alternatives like However, it would be awesome if |
@Papooch It ensures only that you don't declare additional keys. It doesn't ensure everything else. In your example, Joi is happy with a wrong schema: const Car = Joi.object<Car>({
make: Joi.boolean(), // wrong type
// missing key `model`
}); |
@paleo My experience is different with version For example, the following behaves as you describe:
However if I switch to:
gives me a
|
I found this: https://github.com/jbranchaud/til/blob/master/zod/incorporate-existing-type-into-zod-schema.md const customSchema: z.zodType<CustomType> = z.any();
const schema = z.object({
custom: customSchema.array();
}) |
@Athrun-Judah this will only validate that the key type CustomType = {a: 'particular shape'}
const schema = z.object({custom: z.any().array()})
const unvalidated = {custom: ['anything', 3, {at: 'all'}, null,undefined]}
const validated: {custom: CustomType[]} = schema.parse(unvalidated) |
I wrote a tool https://github.com/ylc395/zodify You can use this tool to generate schemas from TypeScript types. |
I'm using a mapped type by defining type IsNullable<T> = Extract<T, null> extends never ? false : true
type IsOptional<T> = Extract<T, undefined> extends never ? false : true
{
type T1 = IsNullable<string> // false
type T2 = IsNullable<string | null> // true
type T3 = IsNullable<string | undefined> // false
type T4 = IsNullable<string | null | undefined> // true
type T5 = IsOptional<string> // false
type T6 = IsOptional<string | null> // false
type T7 = IsOptional<string | undefined> // true
type T8 = IsOptional<string | null | undefined> // true
}
type ZodWithEffects<T extends z.ZodTypeAny> = T | z.ZodEffects<T, unknown, unknown>
export type ToZodSchema<T extends Record<string, any>> = {
[K in keyof T]-?: IsNullable<T[K]> extends true
? ZodWithEffects<z.ZodNullable<z.ZodType<T[K]>>>
: IsOptional<T[K]> extends true
? ZodWithEffects<z.ZodOptional<z.ZodType<T[K]>>>
: ZodWithEffects<z.ZodType<T[K]>>
}
interface Foo {
bar: string
withEffects: string
nullable: number | null
optional?: string
optionalNullable?: string | null
omitted?: string
}
const schema = z.object({
bar: z.string(),
|
any update on this ? how can i use existing interface to construct my zod schema |
@LeulAria There have been multiple suggested solutions in this thread. The TL;DR is that this isn't something that zod does but it can be done with other tools. |
I think this might work -
in this my |
Taking a look at the docs, while this will give proper typing, it will diminish zod's usefulness as a validating tool. Without supplying a custom validator, you can leave out zod completely and have the same value at better readability. |
Thats good, nice @jinyongp ! I took your code and just created a zod.ts file under utils folder and thats it.
|
@schicks solution worked for me until i had
|
@mkatrenik that just taught me so much about how zod holds onto type information on parsers. Very cool! |
@JacobWeisenburger So the solution I ended up using is:
Then in your component,
You can also do Full example,
@LeulAria in case this is useful to you |
is there already any good solutions yet? |
thank you. |
@mkatrenik I asked ChatGPT Premium to resolve this, here's the fully working code
|
Thanks to @HaileyesusZ , whose solution I've enhanced to create this custom type. What is SchemaFromInterface?type SchemaFromInterface<T> = ZodObject<{
[K in keyof Partial<T>]: K extends keyof T ? ZodType<T[K]> : never;
}>; This type alias allows you to create Zod schemas from existing TypeScript interfaces, How to Use It
interface User {
id: string;
name: string;
age: number;
email?: string;
}
const UserSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number(),
email: z.string().email().optional(),
}) satisfies SchemaFromInterface<User>; Benefits
Advanced Example// Define interfaces
interface Product {
id: string;
price: number;
inStock: boolean;
tags?: string[];
}
// Create schema
const ProductSchema = z.object({
id: z.string(),
price: z.number().positive(),
inStock: z.boolean(),
tags: z.array(z.string()).optional(),
}) satisfies SchemaFromInterface<Product>;
// Create partial schema (for updates/patches)
const ProductUpdateSchema = ProductSchema.partial();
// Create type from schema
type ProductFromSchema = z.infer<typeof ProductSchema>; SchemaFromInterface combines TypeScript's type safety with Zod's runtime validation, |
Best solution. |
An here's an even more improved example that works with type Zodd<T> = ZodObject<{
[K in keyof Partial<T>]: K extends keyof T ? ZodType<T[K]> : never;
}>;
export type ZodSatisfies<T> = Zodd<T> | ZodEffects<Zodd<T>>; |
Discussed in #2796
Originally posted by adamerose September 24, 2023
I see Zod described as a "TypeScript-first" library but am struggling to figure out how to actually integrate it with my TypeScript definitions. Is there a way to pass a TypeScript interface to my Zod schema as a generic and have my linter tell me if they're not compatible?
This is how it looks in Yup:
I found discussion here and here but didn't see any solution I could understand, or relied on codegen and external libraries.
I think something like this might work? But it's very boilerplatey and I don't see anything like this in the docs:
The text was updated successfully, but these errors were encountered: