A TypeScript library for validating & parsing structured objects. The API is heavily influenced by Zod's excellent API, while the implementation side aims for the impressive performance of simple-runtypes.
const vehicle = v.union(
v.object({ type: v.literal("plane"), airline: v.string() }),
v.object({ type: v.literal("train") }),
v.object({ type: v.literal("automobile"), make: v.string() })
);
vehicle.parse({ type: "bike" });
// ValitaError: invalid_literal at .type (expected "plane", "train" or "automobile")
Note
While this package is still evolving, we're currently not accepting any new feature requests or suggestions. Please use the issue tracker for bug reports and security concerns, which we highly value and welcome. Thank you for your understanding ❤️
- Input Validation & Parsing: The fundamental goal of the library is to ensure that incoming data, which might not be from a trusted source, aligns with the predetermined format.
- Minimalism: Deliver a streamlined and concentrated library that offers just the essentials.
- Extensibility: Allow users to create their own validators and parsers that cater to specific validation scenarios.
- Data Definition: The library is designed to validate and parse input data as it enters the program, rather than serving as an exhaustive tool for defining all types within the program after obtaining input.
- Extensive Built-In Formats: The library does not prioritize having a large array of built-in validation formats out of the box.
- Asynchronous Parsing: Asynchronous operations are outside the scope for this library.
npm i @badrap/valita
import * as v from "https://deno.land/x/valita/mod.ts";
A motivating example in lack of any better documentation:
import * as v from "@badrap/valita";
const Pet = v.object({
type: v.union(v.literal("dog"), v.literal("cat")),
name: v.string(),
});
const Person = v.object({
name: v.string(),
age: v.number(),
pets: v.array(Pet).optional(),
});
Now Person.parse(value)
returns value
if it matches the Person schema - or throws an error otherwise.
const grizzlor = Person.parse({
name: "Grizzlor",
age: 101,
pets: [
{ type: "cat", name: "Mittens" },
{ type: "cat", name: "Parsley" },
{ type: "cat", name: "Lulu" },
{ type: "cat", name: "Thomas Percival Meowther III" },
],
});
The real magic here comes from TypeScript's type inference. The inferred type for grizzlor
is:
const grizzlor: {
name: string;
age: number;
pets?: { type: "dog" | "cat"; name: string }[] | undefined;
};
You can use Infer<T>
to get your mitts on the inferred type in your code:
type PersonType = v.Infer<typeof Person>;
This section contains an overview of all validation methods.
Let's start with the basics! Like every validation library we support all primitive types like strings
, numbers
, booleans
, null
, undefined
and more.
import * as v from "@badrap/valita";
const developer = v.object({
name: v.string(),
age: v.number(),
usesValita: v.boolean(),
projects: v.array(v.string()),
null: v.null(),
undefined: v.undefined(),
bigNumber: v.bigint(),
});
On top of that we support additional methods to coerce specific values to a certain TypeScript type:
import * as v from "@badrap/valita";
const something = v.object({
shouldNotHappen: v.never(),
noClueWhatThisIs: v.unknown(),
});
When working with APIs/DBs some types are nullable. Valita can validate those types via the .nullable()
method:
import * as v from "@badrap/valita";
// type name = null | string
const name = v.string().nullable();
// Passes
name.parse("Jane Doe");
// Passes
name.parse(null);
One common occurrence when working with APIs is that some fields in an object are optional and therefore can be missing completely. Valita can skip validating those via the .optional()
method:
import * as v from "@badrap/valita";
const person = v.object({
name: v.string(),
// Not everyone filled in their favorite song
song: v.string().optional(),
});
// Passes
person.parse({ name: "Jane Doe", song: "Never gonna give you up" });
// Passes
person.parse({ name: "Jane Doe" });
Whereas .object()
can be used to validate objects, it has the limitation that all property keys have to be known upfront. It doesn't support an arbitrary number of properties, which is where Record types come to the rescue.
import * as v from "@badrap/valita";
// Every property key must be known upfront
const obj = v.object({
foo: v.number(),
bar: v.number(),
});
// ...with records
const obj2 = v.record(v.number());
With the record type you're essentially specifying that there are (theoretically) an infinite number of property keys which are all of type string
. The argument you pass to .record()
is the type of the property values. In the example above all property values are numbers.
Despite JavaScript not having tuple values (yet?), many APIs leverage plain arrays as an escape hatch. For example: If we encode a range between two numbers we'd likely choose type Range = [number, number]
as the data type. From JavaScript's point of view it's just an array but TypeScript knows about the value of each position and that the array must have two entries.
We can express the same with valita:
import * as v from "@badrap/valita";
const range = v.tuple([v.number(), v.number()]);
// Passes
range.parse([1, 2]);
range.parse([200, 2]);
// Fails
range.parse([1]);
range.parse([1, 2, 3]);
range.parse([1, "2"]);
A union type is a value which can have several different representations. Let's imagine we have a value of type Shape
that can be either a circle, a square or a rectangle.
import * as v from "@badrap/valita";
const rectangle = v.object({ type: "rectangle" });
const square = v.object({ type: "square" });
const circle = v.object({ type: "circle" });
// Validates either as "rectangle", "square" or "circle"
const shape = v.union(rectangle, square, circle);
Note that although in this example all representations have a shared property type
, it's not necessary at all. Each representation can be completely different:
import * as v from "@badrap/valita";
const primitive = v.union(v.number(), v.string(), v.boolean());
Sometimes knowing if a value is of a certain type is not enough. We can use the .literal()
method to check for actual values, like checking if a string is either "red"
, "green"
or "blue"
and not just any string.
import * as v from "@badrap/valita";
const rgb = v.union(v.literal("red"), v.literal("green"), v.literal("blue"));
We can also use this to check for concrete numbers, bigint literals or boolean values too:
import * as v from "@badrap/valita";
const banana2YearsOld = v.object({
kind: v.literal("banana"),
age: v.literal(2),
isFresh: v.literal(true),
});
For more complex values you can use the .assert()
-method. Check out the Custom validation functions to learn more about it.
The .assert()
-method can be used for custom validation logic like verifying that a number is inside a certain range for example.
import * as v from "@badrap/valita";
const schema = v
.number()
.assert((v) => v >= 0 && v <= 255, "Must be in between 0 or 255");
A core strength of valita is that validation functions are composable by nature. This means that you can reuse bits and pieces of your schema and compose schemas from smaller building blocks.
import * as v from "@badrap/valita";
// Reusable custom schema for a Person type
const personSchema = v
.object({
name: v.string()
age: v.number(),
})
// Reuse the custom schema times
const schema = v.object({
person: personSchema,
otherPerson: personSchema,
});
Whilst the .assert()
-method is very powerful to check if a type is valid or not, it can only return a boolean value. But sometimes we want to parse our data into a propert type in one go. Imagine a JSON-API which serializes dates as a string
type representing a unix timestamp and we want to return a valid Date
in our validation schema.
{
"created_at": "2022-01-01T00:00:00.000+00:00"
}
This can be done with the .chain()
-method which allows you to cast to a certain type on top of validating the value. It's a more general alternative to .assert()
so to say.
The .chain()
-method receives a function to which it will pass the raw value as the first argument. If the value is deemed as being incorrect we return an error message via v.err("My message")
. If not we cast it to the type we want and pass that to v.ok(value)
.
Similar to .assert()
the .chain()
-method must follow a preceding validation function like v.string()
in our example.
import * as v from "@badrap/valita";
const dateSchema = v.string().chain((str) => {
const date = new Date(str);
// If the date is invalid JS returns NaN here
if (isNaN(date.getTime())) {
return v.err(`Invalid date "${str}"`);
}
return v.ok(date);
});
const schema = v.object({
created_at: dateSchema,
});
You can get access to the property validator with the .shape
property.
const userSchema = v.object({
name: v.string(),
});
const name = userSchema.shape.name.parse("me");
However, if you use userSchema.assert(...)
, then you lose the ability to validate properties. This is intentional because .assert(...)
can change the output type, but .shape
cannot reflect this at runtime. For example:
function isAdmin(x: unknown): x is { name: "admin"; weirdExtraKey: boolean } {
return true;
}
const schema = userSchema.assert(isAdmin);
// @ts-expect-error Property 'shape' does not exist on type 'Type<{ name: "admin"; weirdExtraKey: boolean; }>
schema.shape === undefined;
But that's not a problem! Just declare your property validator outside the object schema.
function isAdmin(x: unknown): boolean {
return true;
}
const adminNameSchema = v.string().assert(isAdmin, 'error_message');
const useSchema = v.object({
name: adminNameSchema
});
const name = adminNameSchema.parse('admin');
One strong suit of valita is its ability to parse types that references itself, like type T = string | T[]
. We can express such a shape with valita using the .lazy()
method.
import * as v from "@badrap/valita";
type T = string | T[];
const myType: v.Type<T> = v.lazy(() => v.union(v.string(), v.array(myType)));
Note that TypeScript needs an explicit type cast as it cannot interfere return types of recursive functions. That's why the variable is typed as v.Type<T>
.
You may come across a schema where fields are related to each other and need to be validated together. Let's pick an example where a JSON-API returns an object with a min
and max
property. In our schema we want to ensure that the min
value is smaller than max
.
{
"min": 10,
"max": 100
}
For that we assert that both properties are of type number
via the .number()
-method. To be able to compare them to each other we add a validation helper on the common parent type of the fields we want to compare. We can leverage the .assert()
-method on the surrounding .object()
result in our example.
import * as v from "@badrap/valita";
const schema = v
.object({
min: v.number(),
max: v.number(),
})
.assert((obj) => obj.min < obj.max, ".min must be smaller than .max");
This library is licensed under the MIT license. See LICENSE.