Schema 101
The validation schema defines the shape and the format of the data you expect during the validation. We have divided the validation schema into three parts, i.e.
- The shape of the top-level object is defined using the
vine.object
method. - The data type for fields is defined using the schema methods like
vine.string
,vine.boolean
, and so on. - Additional validations and mutations are applied using the rules available on a given schema type.
Creating schemas
The validation schema is created using the vine.object
method. The method accepts a key-value pair, where the key is the field name, and the value is another schema type.
const schema = vine.object({
username: vine.string()
})
Re-using and composing schemas
The API for re-using schema types to compose new schemas is intentionally underpowered in VineJS.
We believe, writing some duplicate code to produce simple code can be beneficial over using complex APIs to split, extend, pick, and omit properties that are harder to reason about. You might hold different views, but this is how we want to build and maintain VineJS.
Cloning schema types
VineJS schema APIs mutate the same underlying schema instance. Therefore, you may call the clone
method to create a fresh instance of the same type and configure it separately. For example:
const userSchema = vine.object({
username: vine.string()
})
const postSchema = vine.object({
title: vine.string(),
author: userSchema.clone().nullable()
})
Using existing object properties
You can create a new object and copy properties from an existing object using the object.getProperties
method. The getProperties
method clones existing properties and returns them as a new object.
const userSchema = vine.object({
username: vine.string()
})
const postSchema = vine.object({
title: vine.string(),
author: vine.object({
...userSchema.getProperties(),
id: vine.number(),
})
})
Nullable and optional modifiers
See also: HTML forms and surprises
Throughout the documentation, you will find examples using the nullable
or optional
modifiers. These modifiers are used to mark fields as optional or null.
Optional modifier
All the fields are required by default, and you may mark them as optional using the optional modifier.
- An optional field may contain an
undefined
ornull
value. - The field is removed from the output when it is
undefined
ornull
.
{
name: vine.string().optional()
}
// input=foo; output=foo
// input=null; output=undefined
// input=undefined; output=undefined
Nullable modifier
The nullable modifier expects the field under validation to exist, but its value can be null
. Also, the field is always present in the output.
{
name: vine.string().nullable()
}
// input=foo; output=foo
// input=null; output=null
// input=undefined; throws exception
Using both the modifiers together
You end up with the following behavior when you use the optional
and the nullable
modifiers together.
- Both
null
andundefined
values will be allowed. - If the value is
null
, it will be written to the output. - If the value is
undefined
, it will be removed from the output.
{
name: vine.string().nullable().optional()
}
// input=foo; output=foo
// input=null; output=null
// input=undefined; output=undefined
Schema types
Following is the list of available schema types supported by VineJS. We also support extending the schema API by creating custom types.
- String
- Boolean
- Number
- Union (for expressing conditonals)
- Array
- Tuple
- Object
- Record
- Enum
- Accepted
- Any
- Literal
- Date
Validation metadata
Since VineJS schemas are pre-compiled, you cannot pass runtime options to them. For example, you want the user to enter a credit card number from a specific provider based upon the user's country saved in their profile.
const purchaseValidator = vine.compile(
vine.object({
credit_card: vine
.string()
.creditCard({
provider: [] // SHOULD BE BASED ON USER PROFILE
})
})
)
Assuming you use the purchaseValidator
validator during an HTTP request, you want to fetch the currently logged-in user profile and get the list of credit card providers. In short, you want to define provider
at the time of validating and not at the time of defining the schema.
This is where the validation metadata can help you. You can pass the provider
array as follows.
const user = req.auth.user
// Assuming the user model has the "getProviders" method
const ccProviders = user.getProviders()
await purchaseValidator.validate(req.body, {
meta: {
ccProviders
}
})
Now, let's go to the schema and access the meta.ccProviders
value inside the schema. First, we must pass a callback to the creditCard
validation rule and lazily compute the validation options.
const purchaseValidator = vine.compile(
vine.object({
credit_card: vine
.string()
.creditCard({
provider: []
})
.creditCard((field) => {
return {
provider: field.meta.ccProviders
}
})
})
)
Defining metadata static types
In our previous example, we cannot know that the purchaseValidator
needs the meta.ccProviders
array to be functional. However, we can fix that by defining the static types using the vine.withMetaData
method.
Once you define the static types for the metadata, the TypeScript compiler will force you to provide the same metadata when using the validator.
import { CreditCardOptions } from '@vinejs/vine/types'
type PurchaseValidatorMetaData = {
ccProviders: CreditCardOptions['provider']
}
const purchaseValidator = vine
.withMetaData<PurchaseValidatorMetaData>()
.compile(
vine.object({
credit_card: vine
.string()
.creditCard((field) => {
return {
provider: field.meta.ccProviders
}
})
})
)
// ❌ ERROR: Expected 2 arguments, but got 1.ts(2554)
await purchaseValidator.validate(req.body)
// ❌ ERROR: Property 'meta' is missing in type '{}' but required in type '{ meta: PurchaseValidatorMetaData; }'
await purchaseValidator.validate(req.body, {})
// ✅
await purchaseValidator.validate(req.body, {
meta: {
ccProviders: ['mastercard' as const]
}
})
Using functions as validation rules
You are not only limited to validation rules available via the schema API. You can also convert plain JavaScript functions to validation rules and use them with any schema type.
In the following example, we create a validation rule using the vine.createRule
method and apply it to fields using the schema.use
method.
See also: Creating custom rules
import vine from '@vinejs/vine'
const myRule = vine.createRule(async (value, options, field) => {
// Implementation goes here
})
const schema = vine.object({
username: vine.string().use(
myRule()
),
email: vine.string().email().use(
myRule()
)
})
Bail mode
VineJS stops the validation chain when any validation fails for a given field. We call this behavior the bail
mode. In other libraries, this feature is usually called abort early.
In the following example, if the field's value is not a string
, VineJS will not perform the email
and the unique
validations. This is the behavior you would want most of the time.
const schema = vine.object({
email: vine.string().email().use(
unique({ table: 'users', column: 'email' })
)
})
However, you may turn off the bail
mode for a given field using the bail
method (if needed).
const schema = vine.object({
email: vine.string().email().use(
unique({ table: 'users', column: 'email' })
)
.bail(false)
})
Parsing input value
You may use the parse
method on all the schema types to mutate the input value before the validation cycle begins. Since the parse
method is called before the validation cycle, the field's value is unknown, and you must handle all the edge cases to avoid runtime exceptions.
The parse
method receives the value
as the first argument and the field context as the second argument.
function assignDefaultRole(value: unknown) {
if (!value) {
return 'guest'
}
return value
}
const schema = vine.object({
role: vine.string().parse(assignDefaultRole)
})
Transforming output value
You may use the transform
method on available schema types to mutate the output value. The transform
method is not called in the following cases.
- When the field is invalid (it has failed one or more validations).
- When the field value is
undefined
. - The
transform
method is not available forarray
,object,
record
, andtuple
schema types. This is an intentional limitation, and we may re-consider it if there are enough valid use cases.
The transform
method receives the value
as the first argument and the field context as the second argument. You may return a completely different data type from the transform
method.
const schema = vine.object({
amount: vine.number().decimal([2, 4]).transform((value) => {
return new Amount(value)
})
})
Converting the output to camelCase
VineJS transforms all field names from snake_case
or dash-case
to camelCase
using the object.toCamelCase
modifier.
Considering the complexity of generating accurate static types, we will not add support for other modifiers.
const schema = vine.object({
first_name: vine.string(),
last_name: vine.string(),
referral_code: vine.string().optional()
})
const validate = vine.compile(schema)
const {
first_name,
last_name,
referral_code
} = await validate({ data })
const schema = vine.object({
first_name: vine.string(),
last_name: vine.string(),
referral_code: vine.string().optional()
})
.toCamelCase()
const validate = vine.compile(schema)
const {
first_name,
last_name,
referral_code
firstName,
lastName,
referralCode
} = await validate({ data })