-
-
Notifications
You must be signed in to change notification settings - Fork 216
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
[✨Feature Request]: Add similar functionality like yup.when()
#145
Comments
Thank you for creating this issue. I will try to investigate a |
Hi! Having a |
This enhancement also seems to be exactly what i have been looking for here - #120 |
Here's an idea I had to add a workaround of const schema = object(
{
firstName: string([
minLength(3, "Needs to be at least 3 characters"),
endsWith("cool", "Needs to end with `cool`"),
]),
lastName: nullish(string()),
},
[
when({
field: "lastName",
dependsOn: "firstName",
constraint: ({ lastName }) => lastName?.length > 0 ?? false,
}),
]
); Maybe we could re-write this into something similar to: const schema = object({
firstName: string([
minLength(3, "Needs to be at least 3 characters"),
endsWith("cool", "Needs to end with `cool`"),
]),
lastName: when(string(), {
dependsOn: "firstName",
constraint: ({ lastName }) => lastName?.length > 0 ?? false,
}),
}); |
@demarchenac thank you for your feedback on this. I will think about it. |
I know it might be hard, but it would be really good if we could append another transform based on the condition just like in E.g. type IDType = 'driver_licence' | 'passport';
const schema: ObjectSchema<IdDetailsFormSchema> = yup.object({
primary_id_type: yup.string<'driver_licence' | 'passport'>().required(),
driver_licence_number: yup.string()
.when('primary_id_type', {
is: (value: IDType): boolean => value === 'driver_licence',
then: schema => schema.required(),
otherwise: schema => schema.transform(() => void 0)
})
}); This way, even user still has driver's license number input filled in, it won't be included in the final payload because it's been transformed into This is just a potential add-on, which is good to have. 💚 |
Based on @demarchenac 's idea, we might add another lastName: when(string(), {
dependsOn: "firstName",
constraint: ({ lastName }) => lastName?.length > 0 ?? false,
transform: // ...
// ...: ...
}), |
I took a closer look at Yup's // Validate based on a sibling
const Schema1 = object({
type: enumType(['user', 'admin']),
token: string([
when('sibling', (input) => input?.type === 'admin', {
then: startsWith('admin_'),
else: startsWith('user_'),
}),
]),
});
// Validate based on a child
const Schema2 = object(
{
type: enumType(['user', 'admin']),
token: string(),
},
[
when('child', (input) => input.type === 'admin', {
then: custom((input) => input.token.startsWith('admin_')),
else: custom((input) => input.token.startsWith('user_')),
}),
]
); Instead of a single validation function, it should be possible to pass a pipeline of functions to |
Yeah, this does look good to me, but when using the second case, or not only for the colinhacks/zod#2524 This is an known issue in I'm not sure if this is intended behaviour... as both sides have their valid point, maybe consider about this. This is one of the issue |
Are you sure about that? We revised this a few weeks ago. Edit: Thanks to this comment, I now understand what you mean. |
Disclaimer: just came across this during my research, not very familiar with valibot, only considering it for our project; just wanted to insert my 2 cents. const schema = augmented(
object(
{
auth: enumType(['pass', 'oauth']),
}
),
[
when('child', (obj) => obj.auth === 'pass', {
then: object({ login: string(), password: string() }),
}),
when('child', (obj) => obj.auth === 'oauth', {
then: object({ bearer: string() }),
}),
]
)
/*
scema :: {
auth: 'pass' | 'oauth',
login: 'string' | undefined,
password: 'string' | undefined,
bearer: 'string' | undefined
}
*/ Bad object schema for storing app state, useful when dealing with APIs. |
@Karakatiza666 you should use // Normal union
const Schema1 = union([
object({
auth: literal('pass'),
login: string(),
password: string(),
}),
object({
auth: literal('oauth'),
bearer: string(),
}),
]);
// Discriminated union
const Schema2 = variant('auth', [
object({
auth: literal('pass'),
login: string(),
password: string(),
}),
object({
auth: literal('oauth'),
bearer: string(),
}),
]); |
@fabian-hiller It sounds really helpful for most co-dependent input field validations that I can think of! Lastly, regarding the syntax of your proposal for the P.S: I do see myself using the first syntax more often than the second one |
Thank you for your feedback. Do we even need the second Is ist hard wo make both APIs typesafe. My plan is to type |
Yeah in all the scenarios I'm currently working on the validations are more like the first example of And I agree with it being typed as |
The most common examples that I've been working on are const SomeSchema = object({
...fields
}, [
fieldRequiredWhen(
{
field: { key: 'someField', is: (field) => Boolean(field && field.length > SOME_VALUE /* e.g. 6 */) },
dependsOn: { key: 'sibling' }
},
'This field is required'
),
fieldRequiredWhen(
{
field: { key: 'otherField' },
dependsOn: { key: 'otherSibling', is: (otherSibling) => otherSibling.length >= SOME_LENGTH /* e.g. 8 */ }
},
'This field is required'
),
]) |
Also, I thought of a grouped validation for some fields sharing the const SomeSchema = object({
...fields
}, [
fieldsRequiredWhen(
{
fields: ['dependsOnSibling', 'alsoDependsOnSibling', ...dependantFields, 'lastFieldDependingOnSibling'],
dependsOn: { key: 'sibling', is: (sibling) => sibling === 'some value' }
},
'This field is required' // or something along those lines.
)
]) So, I can definitively see the power that the |
The only issue that I currently have with the provided examples above are the runtime issues in which theses validations are only visible to a user if all of the |
Thank you for your feedback and contribution! I'll give it some thought over the next few days. |
Yeah I totally agree with all the above, and the real difficulty here is to make it type-safe (and I guess that's why
And yeah... this is worrying me. Thanks for the collection.💚 |
I like the idea I proposed in #76 (comment) Code exampleconst schema = object({
name: string(),
password: string(),
confirmPassword: string(),
}, [
{
type: 'pre', // Run before the validation
transform: (data: unknown) => data.password === data.confirmPassword, // Input will be unknown
},
{
type: 'post', // Run after validation (default)
transform: (data: Typed) => data.password === data.confirmPassword, // Input will be typed
// Allow specifying path and error message instead of throwing validation error
path: ["confirmPassword"],
message: "Both password and confirmation must match",
},
// Existing pipes are supported. Are the same as 'post' pipes
custom((data) => data.password === data.confirmPassword),
{
// Object schema only
type: 'partial', // Run after successfull validation of specified fields
fields: ['password', 'confirmPassword'], // Array elements can be typed
transform: (data: Pick<Typed, 'password' | 'confirmPassword'>) => data.password === data.confirmPassword, // Input will contain specified fields
},
]) The only thing What I don't like about non-pipeline solution, is that it is, by definition, cannot be typed. And it requires one field to know the existence of another, that should be the parent schema concern. |
I would be interested to hear what others think about @Demivan's idea. |
@Demivan By non-pipeline solution are you referring to the one that's not within the |
i am not a fan of @Demivan idea. i like the original when child solution proposed it's easy to understand. |
So there are a few things on my mind:
|
@fabian-hiller There is a semantic problem with this approach: when a derived field fails validation (e.g. bearer minLength isn't satisfied) corresponding union branch fails; now another branch is tested, and field 'auth' with value 'oauth' doesn't match the constraint |
Thanks for the hint. For this we probably need something like |
discriminatedUnion is very useful in form validation with different options |
Currently, an object's pipeline is not executed if an issue occurs during validation of its entries. This causes problems, especially with forms, because each field must be valid before the object's pipelines can check the relationships between the values of the fields. I think it makes sense to change this behavior and not run the object's pipeline only when a "type" issue has occurred. For all other issues, which are validated by the pipelines of the entries, we still run the pipeline of the parent object. This would make the behavior similar to |
Yeah exactly. This is a huge issue in |
@fabian-hiller This sounds awesome! |
Just wanted to +1 this issue! Being able to define conditional properties would be awesome for schemas that validate forms with conditional logic. I recently wrote a library for React Hook Forms + Zod (that should be Valibot-compatible as well) where I had to define conditional logic outside of Zod (due to it not supporting this sort of thing), set the corresponding conditional properties in the schema to It would be awesome if Valibot could go a step beyond merely optional properties and actually define conditional properties within the schema. Of course, they'd need to be introspectable so that other libraries could extract the conditional logic (and dependencies) and expose them to the UI. Hope this isn't too off-topic, just wanted to share what I expect would be a very common use-case for this feature. |
How would you design the API for this? I would be happy to see pseudocode. I plan to tackle this on the weekend and next week. |
Good question. There are a couple different parts to this puzzle. First off, how to best define a condition function and its dependencies. You could go the Yup route and do it explicitly, e.g. const condition = [
["sibling1", "sibling2"], // dependencies
(sibling1, sibling2) => sibling1 > sibling2 // condition
] or you could do it implicitly by using some sort of getter that tracked dependencies automatically. Signals libraries tend to this, which is pretty neat, e.g. const condition = (get) => get("sibling1") > get("sibling2") Next, you'll need to decide if the condition applies to a single field or a group of fields in the schema. The former would be simpler, but it is pretty common on forms to have a bunch of fields depend on a single condition. An example of both APIs... // single conditional field example
{
companyName: string(),
companyEmail: conditional(
// show "company email" field if company isn't one we've already seen
get => !knownCompanyNames.includes(get("companyName"))
string([email()]),
)
}
// group of fields sharing the same condition (a "branch" in the schema tree, schema would ideally be a union)
{
companyName: string(),
...conditionalProperties(
// show "company email" and "company location" fields if company isn't one we've already seen
get => !knownCompanyNames.includes(get("companyName")),
{
companyEmail: string([email()]),
companyPostalCode: string([minLength(5)]),
// a nested conditional...
openHours: conditional(
// show "open hours" if company is nearby
get => get("companyPostalCode").startsWith("9720"),
string(),
)
},
)
} There is one glaring omission in these above examples, and that's what actually happens to the schema based on the condition. For form conditional logic, we're just showing & hiding fields (and only validating visible fields) so schema-wise we're either validating a field -or- omitting the field entirely. This is completely different from Zod's refine, which doesn't mutate the schema's shape at all and just adds additional validation. I assume with the above API users would want to be able to mutate the schema, not just add/remove fields. Here's an example of how that might look... // only show "company email" field if company isn't one we've already seen (otherwise ignore & omit it)
{
companyName: string(),
companyEmail: conditional(
get => !knownCompanyNames.includes(get("companyName")),
[ string([email()]), omit() ],
)
} // only require valid "company email" field if company isn't one we've already seen (otherwise it's optional)
{
companyName: string(),
companyEmail: conditional(
get => !knownCompanyNames.includes(get("companyName")),
[ string([email()]), string() ],
)
} or for multiple fields... {
companyName: string(),
...conditionalProperties(
// show "company email" and "company location" fields if company isn't one we've already seen
get => !knownCompanyNames.includes(get("companyName")),
[
{
companyEmail: string([email()]),
companyPostalCode: string([minLength(5)]),
// a nested conditional...
openHours: conditional(
// show "open hours" if company is nearby
get => get("companyPostalCode").startsWith("9720"),
[ string(), omit() ]
)
},
null // don't add these fields if condition is falsy
],
)
} Implementing something like this seems like a real heavy lift and I'd be in way over my head. Impressed by what you've done with this library thus far. Hope these examples are helpful. Regardless of what the final API ends up being, ideally it'd be possible to introspect the schema and extract the condition functions and dependency lists for every conditional schema field. This would make it possible for the schema to be the source of truth for a form with conditional logic, as we'd be able to use it to generate hooks for conditional rendering in the UI. Probably out of scope, but something to keep in mind, as having introspectable schemas means that people can do a lot more with them! |
Thank you for your detailed response. I will take it under consideration. |
Previously, the pipelines of complex schemas were only executed if there were no issues. With the new version v0.22.0, pipelines are now always executed if the input matches the data type of the schema, even if issues have already occurred. In addition, the new method In summary, it is now possible to compare two fields, for example import { custom, email, forward, minLength, object, string } from 'valibot';
const RegisterSchema = object(
{
email: string([
minLength(1, 'Please enter your email.'),
email('The email address is badly formatted.'),
]),
password1: string([
minLength(1, 'Please enter your password.'),
minLength(8, 'Your password must have 8 characters or more.'),
]),
password2: string(),
},
[
forward(
custom(
(input) => input.password1 === input.password2,
'The two passwords do not match.'
),
['password2']
),
]
); I look forward to hearing your feedback on this solution. |
Update: Previously, import * as v from 'valibot';
const RegisterSchema = v.pipe(
v.object({
email: v.pipe(
v.string(),
v.nonEmpty('Please enter your email.'),
v.email('The email address is badly formatted.'),
),
password1: v.pipe(
v.string(),
v.nonEmpty('Please enter your password.'),
v.minLength(8, 'Your password must have 8 characters or more.'),
),
password2: v.string(),
}),
v.forward(
v.partialCheck(
[['password1'], ['password2']],
(input) => input.password1 === input.password2,
'The two passwords do not match.',
),
['password2'],
),
); |
There is an issue with const schema = v.pipe(
v.object({
id: v.pipe(v.string(), v.minLength(3), v.transform(s => Number(s))),
password: v.string(),
}),
v.partialCheck(
[['id'], ['password']],
(input) => {
console.log('Checking', input)
return input.id > 10
},
'Id must be greater than 10',
),
)
const output = v.safeParse(schema, {
id: '123',
password: '123456',
})
// Checking { id: 123, password: '123456' }
const output = v.safeParse(schema, {
id: '12',
password: '123456',
})
// Checking { id: '12', password: '123456' } Input types are incorrect when there in an issue and a transform. I think |
Thanks for reporting @Demivan. This happens because |
I think it is fine to wait for specified fields to be validated. For use cases like password confirmation there really is no need to check confirmation if the password is incorrect anyway. |
Will release a fix in the next hour |
I am closing this issue as I am not sure if a |
Hey, thanks for the great library.
As a validation library,
.when()
API is always a great way to do conditional validating.However, this is not supported in
zod
(colinhacks/zod#1394) (look at how many 👍 there are), and seems thezod
team is 300% 👎 for building this... What a pity😥As I jumped into it for a while, I totally understand that it's pretty hard to maintain the type-safety if this is implemented, but idk if there's a way to do so. As I said, in the end we are validating libraries, of course type-safety is important, but I do think providing such user-friendly API would be much more important.
Currently, I'm using
zod
, and I'd like to switch tovalibot
. When migrating fromyup
tozod
, the lack of.when()
almost killed me☠...I know it's kind of doable via the
pipeline
invalibot
, just likerefine()
/superRefine()
inzod
, but it's still a huge pain... Both are more of a workaround instead of a proper solution.Feel free to say that this is impossible or you are not willing to build that, that's totally fine, as I know the where's the difficulty under the hood, but yeah, what if there's a chance that the dream comes true😂 Thanks💚
If there's any duplicating issues already, please feel free to link them and close this one, as I searched
when
but nothing appears to be there.The text was updated successfully, but these errors were encountered: