Skip to content
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

Add z.select (similar to z.enum but accepting anything as options) #2318

Closed
IlyaSemenov opened this issue Apr 11, 2023 · 4 comments
Closed
Labels

Comments

@IlyaSemenov
Copy link
Contributor

IlyaSemenov commented Apr 11, 2023

Problem

Imagine I have a schema that validates a value to be one of the predefined integers. Like when a user chooses the length of the secret key to be either 512, 1024 or 2048 bits (just random example). Naturally, I would imagine there was a Zod schema for that... like z.enum([512, 1024, 2048])? Unfortunately, that doesn't work, as z.enum only accepts string values.

Another scenario is where the list of options is coming from the variable and is only known in runtime:

const options = await getOptions(); // string[] or number[] or even any[]
const schema = z.enum(options); // doesn't work

Current workaround

One can use z.union([z.literal(512), z.literal(1024), z.literal(2048)] but it's cumbersome, it's arguably slow (as it validates each sub-schema independently) and it doesn't work with option values other than strings and numbers.

Considered solutions

At first, I was thinking that z.enum could be extended to allow any list of anything as valid options (#2317). That is not a viable approach as z.enum(...).enum will break then (thanks @igalklebanov for pointing to that).

Another suggested approach was to extend z.union to accept literals. I don't like that for two reasons:

  • z.union is advertised as an aggregating schema builder. Mixing unrelated concepts (nested schemas vs. literals) should be avoided.
  • If z.union were to accept arbitrary options but treat zod schemas separately, then zod schemas themselves would not be valid options to choose from. (Not that it's a huge tragedy but that is a limitation that could be avoided).

Suggested approach

I propose to add z.select which will be basically z.enum without schema.enum but accepting anything as the list of options (not even necessarily primitives):

const Select = z.select([1, 5, "test", true]) // or: z.select(options) where options extends any[]
type Select = z.infer<typeof Select> // 1 | 5 | "test" | true
Select.options // [1, 5, "test", true]
const option = Select.parse(...) // one of options

In fact, z.enum will probably be a subclass of z.select in that case (they will share schema.options and probably the actual validation implementation).

The resulting type could be derived from the options with ElementOf<...> recipe.

@IlyaSemenov IlyaSemenov changed the title Add z.options (similar to z.enum but accepting anything as options) Add z.option (similar to z.enum but accepting anything as options) Apr 11, 2023
@IlyaSemenov IlyaSemenov changed the title Add z.option (similar to z.enum but accepting anything as options) Add z.select (similar to z.enum but accepting anything as options) Apr 11, 2023
IlyaSemenov added a commit to IlyaSemenov/zod that referenced this issue Apr 17, 2023
@stale
Copy link

stale bot commented Jul 10, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale No activity in last 60 days label Jul 10, 2023
@IlyaSemenov
Copy link
Contributor Author

This is a valid issue with a reasoning and having a PR.

@stale stale bot removed the stale No activity in last 60 days label Jul 12, 2023
@IlyaSemenov
Copy link
Contributor Author

Alas, this has been stuck for a year now. Whoever is interested, I recommend trying valibot which has v.picklist doing the same:

import * as v from "valibot"

const options = await getOptions(); // string[] or number[] or even any[]
const schema = v.picklist(options); // works!

const input = v.parse(schema, data) // input is guaranteed to be one of options, properly typed

@colinhacks
Copy link
Owner

Thanks for the great PR and analysis @IlyaSemenov.

I'm wary of a generic oneOf or select schema that allows non-literals. Deep equality is a can of worms I'd rather not open.

There are a couple solutions I prefer here.

1. Support for numbers and symbols in z.enum()

I have this working in a branch, and ZodEnum.enum can handle numerical values just fine.

z.enum(["asdf", 1234]).enum[1234]; // 1243

Unfortunately the plain symbol keys aren't represented in the .enum but there's nothing to be done for that.

2. Support an array of values in z.literal() (my pick)

This just occurred to me after I'd finished my work on #1. I feel good about it. It's a simple (albeit breaking) extension to ZodLiteral. I think it makes the most sense and doesn't mess with the "purity" of ZodEnum.

This is a breaking change so I plan to merge it in Zod 4. Happy to hear any counterarguments or alternatives! Closing this for now because the v4 branch has already diverged wildly from master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants