Friday, January 12, 2024
Migrating from Zod to Valibot: A Comparative Experience
I've recently migrated the validation part of the contact form of my website (a.k.a. this website) from Zod to Valibot. And I would like to share with you the experience.
What is Valibot?
Valibot is commonly known as the "Zod alternative with a smaller bundle size". Similar to Zod, it is a schema validation library. It is aimed at validating unknown data and rejects it if it doesn’t meet the predefined format. Consider it as TypeScript, but the validation happens in runtime while allowing more complicated “rules” to be set up.
Here are some common use cases:
- Client-side form validation
- API input validation, e.g. request body, query parameters, etc.
Why should I use Valibot instead of millions of other similar libraries?
What sets Valibot apart from similar libraries, including but not limited to Zod and Yup, is the modular design of Valibot’s APIs. Traditionally, schema validation libraries make use of method chaining to define validation rules, for example, this is how we create an email address validator with Zod:
One problem of this API design is that upon importing the
string() function, all string-related validators (IP address, UUID, etc.) will be included in your bundle, no matter whether we are using those features or not. This creates unnecessary bloat and can impact the performance of applications, especially when it is used on the client side, which is more sensitive to bundle size.
In comparison, this is how we can implement the same thing with Valibot:
Unlike the previous code snippet, the
string() function here only includes the bare minimum for checking whether the input data is a string and nothing else. Additional functionalities are enabled via importing other validators, which Valibot refers to as “pipes”. This ensures only the necessary logic is included in our bundle.
My use case
As mentioned, my use case is the contact form of this website. The business logic is as follows:
- The form consists of 4 text fields: name, email, subject, and message; and 1 checkbox: "Show my message in the guestbook."
- The email field, when filled, should be in a valid email format.
- When the checkbox is unchecked, all fields should be nonempty, i.e. cannot be
- When the checkbox is checked, the email and subject fields are optional, while the remaining fields stay nonempty.
This is how my schema looked like before migration, implemented using Zod. It is used for both client-side and server-side validation.
And this is how my schema currently looks like with Valibot.
Allow me to elaborate on the experience of getting from here, to there.
Bundle size is as small as advertised
In my case, after finishing the migration, the bundle size of the two libraries are as follows.
That's a nearly 90% size reduction!!!
Obviously, my use case is not the most complex in the world, and the size reduction will diminish as more and more different validators are used, but from my experience, we usually only need a subset of validators on the client side, so chances are you will still get the benefit of (significantly) smaller bundle size.
API is almost a one-to-one mapping to Zod
As shown in the above code snippet, the APIs of Valibot look very familiar if you have experience in other popular schema validation libraries. The author, Fabian Hiller, has taken heavy inspiration from various schema validation libraries when designing the APIs.
I find myself constantly in an "OK, I've been using
z.someFunc() from Zod, what's the equivalent of that in Valibot?" mindset while migrating, and that works out pretty well.
The author is open-minded and friendly
The most common GitHub issues in the Valibot repo, besides bug reports, are feature requests. Usually, these consist of "Hey Fabian, would you consider adding X feature that is available in Yup/Zod?". And the answer to that question is typically a yes followed by a quick PR. This is also one of the reasons why Valibot gets so feature-rich in such a short period.
This attitude also applies when answering questions from beginners like me. Unlike popular alternatives, Valibot doesn't have the large user base to support the classic "ask a question on Reddit and someone will answer" model right now. Instead, Fabian is being fairly active on GitHub discussion, answering questions, finding out why such a question is being asked, and where the confusion comes from, while seeing those as opportunities to further improve Valibot.
Here is me trying to ask a noob question while figuring out the API, when Fabian jumps in: https://github.com/fabian-hiller/valibot/discussions/362
The Bad (at least at this stage)
The documentation is seriously lacking
A lot of the pages in the API Reference are simply stubs. This creates considerable difficulties while working on Valibot, especially since some of the functions are named and work (slightly) differently.
For instance, I was looking for the equivalent of
or() from Zod in Valibot. And I wasn't aware that
merge() is in fact
or in both libraries. This takes me quite a while since I have to guess what each of the functions (there are a lot) does by its name and comment and take a look at the source code to prove my assumption.
After that, I composed the following code for validating the optional email field. A one-to-one mapping of my original Zod schema. Note that the field also accepts
'', because that needs to be the default value of a controlled MUI Joy UI
The schema's logic works as expected, but a generic error message is returned, instead of the custom
'Not a valid email' message when the email format is incorrect. As it turns out, the way
union() works is whenever any of the associated validators fail,
union() itself will also throw an error, and it is its error message being returned. Hence, the correct way of doing it should be:
This behavior is currently undocumented and would require some try and error or studying the source code before knowing it.
Update: 18 Jan, 2024
The reception of this article turns out to be quite positive and I’m pleasantly surprised that Fabian has shared this article on X. Here are some updates to Valibot since then.
- Starting from v0.26.0, the expected behavior for
union()becomes the previously mentioned "wrong code". i.e.
- Fabian decided to switch focus to completing the documentation for non-async APIs, target by the end of January.
Valibot has a lot of potential to be the Zod/Yup replacement, but the documentation quality is its biggest blocker. I would imagine enthusiasts would be interested in trying out Valibot while enjoying the smaller bundle size. And I highly recommend you check it out if you fit the description.
On the other hand, Valibot might be too early for bigger, more serious projects to adopt as the lack of documentation is a risk that can't be overlooked.
Moving forward, I would personally suggest the author and the maintainer(s) focus on the documentation quality instead of adding more features or implementing API changes (yes, there are already new and deprecated APIs respectively at the time this article is written). The feature set is comprehensive enough to compete with the incumbents. What the team should do is to make it easier for people to learn how to use them.