Optionals
It often happens that undefined or null should also be accepted instead of the value. To make the API more readable for this and to reduce boilerplate, Valibot offers a shortcut for this functionality with optional, exactOptional, undefinedable, nullable and nullish.
How it works
To accept undefined and/or null besides your actual value, you just have to wrap the schema in optional, exactOptional, undefinedable, nullable or nullish.
Note:
exactOptionalallows missing entries in objects, but does not allowundefinedas a specified value.
import * as v from 'valibot';
const OptionalStringSchema = v.optional(v.string()); // string | undefined
const ExactOptionalStringSchema = v.exactOptional(v.string()); // string
const UndefinedableStringSchema = v.undefinedable(v.string()); // string | undefined
const NullableStringSchema = v.nullable(v.string()); // string | null
const NullishStringSchema = v.nullish(v.string()); // string | null | undefined
Use in objects
When used inside objects, optional, exactOptional, and nullish are a special case, as they also mark the key as optional in TypeScript with a question mark.
import * as v from 'valibot';
const OptionalKeySchema = v.object({ key: v.optional(v.string()) }); // { key?: string | undefined }
Default values
What makes optional, exactOptional, undefinedable, nullable and nullish unique is that the schema functions accept a default value as the second argument. Depending on the schema function, this default value is always used if the input is missing, undefined or null.
import * as v from 'valibot';
const OptionalStringSchema = v.optional(v.string(), "I'm the default!");
type OptionalStringInput = v.InferInput<typeof OptionalStringSchema>; // string | undefined
type OptionalStringOutput = v.InferOutput<typeof OptionalStringSchema>; // string
By providing a default value, the input type of the schema now differs from the output type. The schema in the example now accepts string and undefined as input, but returns a string as output in both cases.
Dynamic default values
In some cases it is necessary to generate the default value dynamically. For this purpose, a function that generates and returns the default value can also be passed as the second argument.
import * as v from 'valibot';
const NullableDateSchema = v.nullable(v.date(), () => new Date());
The previous example thus creates a new instance of the Date class for each validation with null as input, which is then used as the default value.
If you want missing object entries to default to an
undefinedvalue, pass a function that returnsundefinedas the second argument:v.optional(v.string(), () => undefined). This ensures the key is always included in the output, since omitting the default would cause missing keys to be skipped entirely.
Dependent default values
In rare cases, a default value for an optional entry may depend on the values of another entries in the same object. This can be achieved by using transform in the pipe of the object.
import * as v from 'valibot';
const CalculationSchema = v.pipe(
v.object({
a: v.number(),
b: v.number(),
sum: v.optional(v.number()),
}),
v.transform((input) => ({
...input,
sum: input.sum === undefined ? input.a + input.b : input.sum,
}))
);
Pipe execution behavior
When an object entry uses optional, exactOptional, or nullish inside a pipe, it is important to understand when the pipe executes.
Without default values
If no default_ value is provided, missing object keys are completely ignored and their pipes will not be executed. If the key is present with undefined or null, the pipe still runs (but it may return an issue depending on the schema used).
import * as v from 'valibot';
const Schema = v.object({
value: v.pipe(
v.optional(v.string()),
v.transform((input) => input.toUpperCase()) // Does not run for missing keys
),
});
const result = v.parse(Schema, {}); // Output: {}
With default values
When a default_ value is provided, the pipe will execute for missing keys using the default value.
import * as v from 'valibot';
const Schema = v.object({
value: v.pipe(
v.optional(v.string(), 'hello'), // Default value provided
v.transform((input) => input.toUpperCase()) // Runs with 'hello' for missing keys too
),
});
const result = v.parse(Schema, {}); // Output: { value: 'HELLO' }
This behavior ensures that the output type is consistent and transforms can reliably process values.