Zod: create a schema using an existing type
Asked Answered
S

5

33

I have an endpoint that should get a parameter method which should comply with the Axios type Method.

How can I create a schema with Zod that validates that the value is using the type Schema?

import { Method } from 'axios';

const Schema = zod.object({
  method: zod.someHowUseTheTypeFrom(Method),
});

The type of Method from the Axios package is:

export type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'purge' | 'PURGE'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK'
Subbase answered 7/4, 2022 at 12:55 Comment(3)
zod.string().regex(/^(get|delete|...)$/) was the best I could do with the documentation on the READMEMatchbook
@kellys thanks. I also found zod.enum(['get','GET',...]), but I prefer to use the type directlySubbase
@Dotan, have you found a way to do this directly with existing types or maybe another way that doesn't require using a Zod method like z.enum(...)? Thanks.Vikiviking
K
29

Reading your comment it sounds like you want to ensure that your schema is in sync with the Method type from axios. I would suggest doing the following:

import { z } from 'zod';
import type { Method } from 'axios';

const methods: z.ZodType<Method> = z.enum(['get', 'GET', ...]);

Which will at least enforce that the schema on the right hand side of the expression will parse valid axios Method results. Unfortunately, anything more may be out of reach unless axios also exports an array containing the strings that correspond to the values in the Method type.

The original thing that you were looking for z.something(<type here>) can't work because zod is using actual runtime objects, and types like Method don't exist at runtime. If axios exported an array containing the methods, then that would be a runtime value and you could use that (perhaps with some type casting) to generate your methods schema (more on this in a moment).

The other shortcoming to this approach is that something like this will typecheck:

const methods z.ZodType<Method> = z.enum(['get']);

The reason for that is because of how types work in TypeScript. That enum schema will only ever parse successfully for 'get' but because the literal 'get' is a subtype of the larger union type defined in Method, the resulting schema is also assignable.

So, the next option I'm going to pose feels slightly self-defeating in that it's going to require redeclaring all the values in Method, however, you can continue to use the axios Method type and you will definitely have a schema that parses all of the values in Method (ie, does not succumb to the issue mentioned above):

import { z } from "zod";
import { Method } from "axios";

const METHOD_MAP: { [K in Method]: null } = {
  get: null,
  GET: null,
  delete: null,
  DELETE: null,
  head: null,
  HEAD: null,
  options: null,
  OPTIONS: null,
  post: null,
  POST: null,
  put: null,
  PUT: null,
  patch: null,
  PATCH: null,
  purge: null,
  PURGE: null,
  link: null,
  LINK: null,
  unlink: null,
  UNLINK: null
};

const METHODS = (Object.keys(METHOD_MAP) as unknown) as readonly [
  Method,
  ...Method[]
];
const methods: z.ZodType<Method> = z.enum(METHODS);

The type assertion for METHODS is safe here because the METHODS_MAP is not exported and we know exactly what keys it has. Now, the METHOD_MAP object will cause a type error if any Method value is missing though which means the resulting schema will parse all Method values as a guarantee enforced at compile time.

Knowhow answered 11/5, 2022 at 2:43 Comment(4)
Thanks for the solution! Is there a reason why you are not skipping METHODS and use z.nativeEnum instead? In your example, can't you just do z.nativeEnum(METHOD_MAP), assuming you also set a string value on each entry of the METHOD_MAP? Would need to change its type to { [K in Method]: K } as well.Rauscher
@Knowhow Do you know how to export just an a array of string from a package you have control over wihtout bringing it all as dependancy ?Amaro
I'm not sure of your exact situation, but you may need to split the package into two parts if you don't want to include the entire package as a dependency. Depending on where you're running your code and how the code is transpiled, you may not need to worry too much about including an entire package just for a constants array since unused code may be dropped by the tree shaker. Hard to say more though without details. It might make sense for you to ask a new top level question.Knowhow
@BennettDams No reason, your suggestion with z.nativeEnum looks like an improvement. I guess one reason to keep it is if you want a list of all methods for some reason, but if not then nativeEnum seems like less code and fewer type assertions.Knowhow
B
15

I have found out that using z.custom<ExistingType>() is working for me, and would be appropriate for this kind of problem.

[Edited] According to @esteban-toress, we still have to add a validation function at the end like this z.custom<ExistingType>((value) => //do something & return a boolean). Otherwise, just z.custom<ExistingType>() only returns ZodAny type which allows any value.

[Edited] This solution is only preferable for type inferences if you wanted to use this to please Typescript types, but I would not recommend this for Zod validation due to the above behavior.

See docs: https://zod.dev/?id=custom-schemas

Blintz answered 10/8, 2023 at 3:49 Comment(3)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Fenestella
This works but worth noticing that you will need to add a validation function at the end otherwise zod will allow any value zod.dev/?id=custom-schemasCristicristian
best answer honestlyCalcaneus
M
2

If you want to use the type directly you can use this:

const methods = ['get','GET',...] as const;

export type Method = (typeof methods)[number];

zod.enum(methods);

You get the best of both worlds this way; having the methods in a value you can use (array), and the type that you originally wanted.

Matchbook answered 7/4, 2022 at 22:10 Comment(2)
That's cool but what I was going for is to use the Method type directly from axios, so that I don't have to repeat it, or at least I have typeScript verify that I use the correct type.Subbase
@Subbase I have a same thought. I though zod can reduce my time on calling request param type check, but now I fond out I have to do a repeated work, already done in typescript type defination, but in a zod way, to support runtime validation.Melloney
T
1

You can use ts-to-zod to export your typescript type into a zod schema.

Threecolor answered 29/5, 2023 at 15:41 Comment(1)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewEnfield
D
0

if you'v existing type from say an orm schema or gql generated or some sort and you want to add zod currently i found this working from me.

export type InferZodMap<T extends abstract new (...args: any) => any> = {
    [k in keyof Partial<InstanceType<T>>]?: unknown;
};


// then use it like

type User {
    email: string;
}

const UserInsertValidation = z.object({
  email: z.string(),
} satisfies InferZodMap<typeof User>);

Dulcimer answered 15/6, 2024 at 1:51 Comment(2)
How would this work? User is a type, so how can you use typeof User here? Its not a value.Denigrate
we are using satisfies aka the object should satisfies the type.Dulcimer

© 2022 - 2025 — McMap. All rights reserved.