valibot
valibot copied to clipboard
Is `object` a footgun?
The documentation for the object() schema states:
The
objectschema removes unknown entries. This means that entries that you have not defined in the first argument are not validated and added to the output. You can change this behavior by using thelooseObjectorstrictObjectschema instead.
This can lead to some surprising behavior when all the object's properties are optional. An object schema with all-optional properties will successfully parse any value that is an object, and convert it into an empty object.
The first way this manifests is around arrays. An object schema with all-optional properties will successfully parse an array into an empty object:
const Schema = v.object({
foo: v.optional(v.string())
});
const result = v.parse(Schema, [1, 2, 3, 4, 5]);
console.log(result);
[log]: {}
If we try this with looseObject, it converts the array into a plain object with numeric properties (this is also surprising!):
[log]: {
0: 1,
1: 2,
2: 3,
3: 4,
4: 5
}
And strictObject fails as expected.
The second surprising case is around unions, and was brought up in #1317. If an object schema with all-optional properties is put into a union, it'll always succeed, and may strip properties present in other union branches:
let D = v.union([v.object({ a1: v.optional(v.string()) }), v.object({ a2: v.optional(v.string()) })]);
console.log(v.parse(D, { a2: '222' }));
[log]: {}
In this case, both looseObject and strictObject will return the correct result: looseObject will not strip a2, and strictObject will make the first union branch fail to parse and move on to the second.
For a future version of Valibot, I think the following API would result in the fewest footguns:
- Remove
objectentirely. Silently stripping unknown properties just does too many weird things. - Change
looseObjectso it does not re-create the object. This would make it truly structurally typed, and avoid the surprising array behavior mentioned above. - Keep
strictObjectas-is.
I should also mention that it was very surprising to discover that Valibot re-creates every object it parses! All its documentation heavily refers to it as a "validation" library, and I don't expect data to change shape when it is "validated".
Hey, Valibot parses complex data that may contain nested schemas without modifying the original input. This is useful when applying transformations, for example. An object forms the basis of all more specialized, complex data structures, such as arrays. Therefore, an array is a valid object. Initially, it was implemented with stricter validation, but that resulted in many strange edge cases. Have you encountered any real-world problems where Valibot returns an unexpected result and breaks your code besides the union case (because this is a different topic I think).
So far, just the union case (https://github.com/valadaptive/glypht/pull/8).
Hey, Valibot parses complex data that may contain nested schemas without modifying the original input. This is useful when applying transformations, for example.
This makes sense. Maybe just go through the guides and replace all the usages of "validate" with "parse"? That would make it a bit more clear it actually returns entirely new objects that may be missing properties from the originals. My gripe is that you really have to read between the lines to realize that "validating" an object actually means copying all its properties over to an entirely new one and returning that, even if your entire schema contains no transformations.
An object forms the basis of all more specialized, complex data structures, such as arrays. Therefore, an array is a valid object. Initially, it was implemented with stricter validation, but that resulted in many strange edge cases.
I'm curious what kind of edge cases this caused. It seems to me that far more edge cases are caused by turning arrays into plain objects. If I created a REST API, and its schema says that a certain field needs to be an object, I'd expect my validation library to reject requests that provided, say, an array of numbers.
TypeScript detects and rejects these sorts of situations via weak type detection. If you write the following TypeScript code:
type Foo = {
bar?: string;
};
function baz(arg: Foo) {
console.log(arg?.bar);
}
baz([1, 2, 5]);
it will fail to typecheck because of it.
Compare Zod's documentation (emphasis not mine):
Given any Zod schema, use
.parseto validate an input. If it's valid, Zod returns a strongly-typed deep clone of the input.
It seems that Zod does reject arrays in place of objects, with an "Invalid input: expected object, received array" error.