valibot
valibot copied to clipboard
Problem with intersecting looseObjects with optional pipes
So I just found weird behavior when trying to instersect two looseObjects with optional pipes. Great combo, I know. The problem is that changing the combination sligthly (one object + one loose object, one pipe + one no pipe) makes it work.
I described all scenarios in the playground
Archive
import * as v from 'valibot';
console.log(
'both object with optional pipe',
v.safeParse(
v.intersect([
v.object({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
}),
v.object({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
}),
]),
{
firstName: 'BOB',
},
),
);
console.log(
'both looseObject with optional pipe',
v.safeParse(
v.intersect([
v.looseObject({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
}),
v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
}),
]),
{
firstName: 'BOB',
},
),
);
console.log(
'one object, one looseObject',
v.safeParse(
v.intersect([
v.object({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
}),
v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
}),
]),
{
firstName: 'BOB',
},
),
);
console.log(
'one required pipe, one optional pipe',
v.safeParse(
v.intersect([
v.looseObject({
firstName: v.pipe(v.string(), v.toLowerCase()),
}),
v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
}),
]),
{
firstName: 'BOB',
},
),
);
console.log(
'one optional string, one optional pipe',
v.safeParse(
v.intersect([
v.looseObject({
firstName: v.optional(v.string()),
}),
v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
}),
]),
{
firstName: 'BOB',
},
),
);
Originally discovered on discord thread
One interesting thing I found when discovering this: it's the _merge function trying to compare a value before and after the pipe: https://github.com/fabian-hiller/valibot/blob/fd9189064831451f1f97c2f94b7afd9a0fca26dd/library/src/schemas/intersect/intersect.ts#L149
It kind of makes sense that it doesn't work if you think about it.
Since both schemas are looseObjects, firstName will be included in the outputs of both schemas. First schema correctly catching it as a known property and doing the transform, the other catching is as a "loose" property and not doing anything to it. Hence it is trying to compare bob (after the transform) to BOB (the original input).
My hypothetical fix would be to ignore the "loose" attribute of the object when intersecting. Treat all looseObject as object for the intersection purposes, apply loose at the end if any of the intersect objects is loose.
Thank you for reaching out! I investigate this issue and would also say that this is expected. The value of firstName is different. That's why merging both objects in not possible. Our error message in this cases is a bit confusing but I don't know how to make it better without breaking out of our uniformed issue and error message structure.
Do you think there is a way to make looseObjects compatible with intersect? Or is it just a design limitation? I guess it won't be too hard to work around this problem in user-land 🤔
The problem is not looseObject to problem it the transformation. Yes, there is a workaround. One option would be to use the entries of your looseObject to create a new object schemas when using intersect:
import * as v from 'valibot';
const Schema1 = v.looseObject({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
});
const Schema2 = v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
});
const Schema3 = v.intersect([
v.object(Schema1.entries),
v.object(Schema2.entries),
]);
Also, keep in mind that you can merge object directly:
import * as v from 'valibot';
const Schema1 = v.looseObject({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
});
const Schema2 = v.looseObject({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
});
const Schema3 = v.object({ ...Schema1.entries, ...Schema2.entries });
That's not an ideal solution if either of the schemas is itself a pipe :D
Although my original problem was slightly different. I had all schemas as v.object but it caused loose params to get cut off. Right now I'm doing something like:
const Schema1 = v.object({
firstName: v.optional(v.pipe(v.string(), v.toLowerCase())),
});
const Schema2 = v.object({
lastName: v.optional(v.pipe(v.string(), v.toUpperCase())),
});
const Schema3 = v.intersect([Schema1, Schema2]);
const output = { ...input, ...v.parse(Schema3, input)};
That's not an ideal solution if either of the schemas is itself a pipe :D
Unfortunately, this is true. We do not yet have a perfect solution for this yet. 😐
const output = { ...input, ...v.parse(Schema3, input)}
This works, but you must be careful when processing the output, as it may contain malicious additional object entries.
It's kind of the point, but what makes them malicious?
If you put the object directly into your DB (without assigning each known property individually), users could add arbitrary properties and spam your DB.
Hi, @KubaJastrz. I'm Dosu, and I'm helping the Valibot team manage their backlog. I'm marking this issue as stale.
Issue Summary:
- You reported an issue with intersecting two
looseObjectsusing optional pipes, causing problems due to the_mergefunction. - Suggested treating all
looseObjectsas regular objects during intersection and applying the loose attribute afterward. - Fabian-hiller acknowledged the issue as expected behavior and suggested workarounds, but you noted limitations with these solutions.
- Concerns were raised about potential security risks when merging objects directly.
Next Steps:
- Please let me know if this issue is still relevant to the latest version of the Valibot repository by commenting on this issue.
- If there is no further activity, this issue will be automatically closed in 30 days.
Thank you for your understanding and contribution!