This is a guest post by Fabian Hiller.
I am pleased to announce, with support from Miško Hevery and Ryan Carniato, my new open source project Valibot. Valibot is a schema library for validating structural data, comparable to Zod, Ajv, Joi, and Yup.
The big innovation of Valibot is the modular design of the API and an optimization of the source code for compression.
This new approach enables unprecedented bundle size minimization through code splitting and compression, making it a perfect complement to current innovations in the fullstack space.
Valibot has no dependencies and can basically be used in any JavaScript or TypeScript project to validate data against a schema.
Together with my supervisors I would like to introduce Valibot to you in this article. We will explain how the library works, how it differs from other solutions and which use cases it is suitable for.
Hi, I'm Fabian, author of Modular Forms and thus part of the SolidJS, Qwik and Preact ecosystem. Apart from that, I teach responsive webdesign with HTML and CSS at heise Academy and occasionally talk about various web technologies like React, Next.js and Vue on the programmier.bar podcast.
As part of my bachelor thesis at the Stuttgart Media University, supervised by professor Walter Kriha as well as Miško and Ryan, I investigated different schema libraries and developed with Valibot a novel schema library from scratch.
I also incorporated the experience I gained in the last months developing a modular and type-safe form library with my first open source project, Modular Forms.
The core function of Valibot is to create a schema. A schema can be compared to a type definition in TypeScript. The big difference is that TypeScript types are "not executed" and are more or less a DX feature. A schema on the other hand, apart from the inferred type definition, can also be executed at runtime to guarantee type safety of unknown data.
Similar to how types can be defined in TypeScript, Valibot allows you to define a schema with various small functions. This applies to primitive values like strings as well as to more complex data sets like objects.
In addition, the library helps to perform more detailed validations and transformations with the help of pipelines. Thus, for example, it can be ensured that a string is an email and ends with a certain domain.
import { email, endsWith, string } from "valibot";
const EmailSchema = string([email(), endsWith("@example.com")]);
Valibot offers almost the same options as TypeScript. For example, you can make the values of an object optional with partial
or make them required with required
. With merge
, you can join multiple object schemas and with pick
or omit
, you can include or exclude certain values of an existing schema.
import { number, object, partial, pick, string } from "valibot";
// TypeScript
type Object1 = Partial<{ key1: string; key2: number }>;
// Valibot
const object1 = partial(object({ key1: string(), key2: number() }));
// TypeScript
type Object2 = Pick<Object1, "key1">;
// Valibot
const object2 = pick(object1, ["key1"]);
Valibot is fully type-safe and allows you to infer the input and output type of a schema. The input and output of a schema differs only if you use transform
to transform the data after validation. Therefore, in most cases you will only be interested in the output.
import { type Output, email, minLength, object, string } from "valibot";
const LoginSchema = object({
email: string([email()]),
password: string([minLength(8)]),
});
type LoginData = Output<typeof LoginSchema>; // { email: string; password: string }
Now to parse unknown data using a schema, the parse
function is used. Valibot also supports asyncronous validation with parseAsync
. If the data does not match the schema, an error is thrown with useful information to fix the problem. If no error is thrown, the data conforms to the schema and is returned typed.
import { parse } from "valibot";
parse(LoginSchema, 123456); // throws error
parse(LoginSchema, { email: "", password: "" }); // throws error
parse(LoginSchema, { password: "12345678" }); // throws error
const loginData = parse(LoginSchema, {
email: "[email protected]",
password: "12345678",
}); // as { email: string; password: string }
Since we use Zod ourselves in various projects and it is pretty much standard among newer TypeScript projects, Valibot's API design is partly based on it. Thank you, Colin McDonnell, for this great library and the positive influence in the ecosystem.
Even though the API resembles other solutions at first glance, the implementation and structure of the source code is very different. In the following, we would like to highlight the differences that can be beneficial for both developers and end users.
Instead of relying on a few large functions with many methods, Valibot's API design and source code is based on many small and independent functions, each with just a single task. This modular design has several advantages.
On the one hand, the functionality of the library can be easily extended with external code. On the other, it makes the source code more robust and secure because the functionality of the individual functions as well as special edge cases can be tested much easier through unit tests.
However, perhaps the biggest advantage is that a bundler can use the import statements to remove any code that is not needed. Thus, only the code that is actually used ends up in the production build. This allows us to extend the functionality of Valibot with additional functions without increasing the bundle size for all users.
This can make a big difference, especially for client-side validation, as it reduces the bundle size and, depending on the framework, speeds up the startup time.
import { email, minLength, object, string } from "valibot"; // 0.7 KB
const LoginSchema = object({
email: string([
minLength(1, "Please enter your email."),
email("The email address is badly formatted."),
]),
password: string([
minLength(1, "Please enter your password."),
minLength(8, "You password must have 8 characters or more."),
]),
});
For example, to validate a simple login form, Zod requires 11.51 KB whereas Valibot requires only 0.7 KB. That's a 94 % reduction in bundle size. This is due to the fact that Zod's functions have several methods with additional functionality that cannot be easily removed by current bundlers when they are not executed in your source code.
import { object, string } from "zod"; // 11.51 KB
const LoginSchema = object({
email: string()
.min(1, "Please enter your email.")
.email("The email address is badly formatted."),
password: string()
.min(1, "Please enter your password.")
.min(8, "You password must have 8 characters or more."),
});
Besides the individual bundle size, the overall size of the library is also significantly smaller. This is due to the fact that Valibot's source code is simpler in structure, less complicated and optimized for compression. To be fair, in the following comparison we must take into account that the functionality between the listed libraries is different and this can have a big impact on the final numbers.
Next, we would like to point out some use cases for which Valibot is particularly well suited. We welcome ideas for other use cases that we may not have thought of yet.
Since most API endpoints can be reached via the Internet, basically anyone can send a request and transmit data. It is therefore important to apply zero trust security and to check request data thoroughly before processing it further.
This works particularly well with a schema, compared to if/else conditions, as even complex structures can be easily mapped. In addition, Valibot automatically types the parsed data according to the schema, which improves type safety and thus makes your code more secure.
A schema can also be used for form validation. Due to the small bundle size and the possibility to individualize the error messages, Valibot is particularly well suited for this. Also, fullstack frameworks like Next.js, Remix, and Nuxt allow the same schema to be used for validation in the browser as well as on the server, which reduces your code to the minimum.
import { parse } from "valibot";
import { loginUser } from "~/api";
import { LoginSchema } from "~/schemas";
export default function LoginRoute() {
async function login(formData: FormData) {
"use server";
try {
const { email, password } = parse(
LoginSchema,
Object.fromEntries(formData.entries())
);
await loginUser({ email, password });
} catch (error) {
// Handle errors
}
}
return (
<form action={login}>
<input name="email" type="email" required />
<input name="password" type="password" required minLength={8} />
<button type="submit">Login</button>
</form>
);
}
Another code example in combination with a form library can be found here.
Library authors can also make use of Valibot, for example, to match configuration files with a schema and, in the event of an error, provide clear indications of the cause and how to fix the problem. The same applies to environment variables to quickly detect configuration errors.
Valibot already covers most of the functionality of comparable schema libraries. With the release of v0.1, we want to encourage content creators and early adopters to test the library and provide feedback. Valibot has a test coverage of 100%, which means you can use it freely in smaller projects.
In the next weeks, we will integrate Valibot into Qwik and various projects in the SolidJS ecosystem. We are also happy to support other library authors who want to integrate Valibot into their projects. For tRPC, React Hook Form, FormKit, Conform and TanStack Forms, just to name a few, an integration could be interesting.
Besides the integration into other projects, the documentation of the library has the highest priority. Since I am writing my bachelor thesis in parallel, I assume that it will take some time until the documentation is complete.
The source code of Valibot is well structured and extensively commented. Therefore the GitHub repository can be used as a fallback.
Many thanks to David Di Biase, who read the article in advance and gave us detailed feedback.
Introducing Visual Copilot: convert Figma designs to high quality code in a single click.