Validators
Overview
Validators provide field-level data integrity checks that run automatically during create and update operations. The @Validator decorator attaches a custom validation function to a field, while @Mandatory ensures that specific fields are present in request bodies for given operations. The @Optional decorator explicitly marks a field as not mandatory.
The @Validator Decorator
Basic Usage
The @Validator decorator accepts a function that receives the field value and returns true if valid or false if invalid. It can instead return a parse result (see Parsed Values) so that the parsed value is written. The function can also return a Promise of either for asynchronous validation.
import { Validator } from "@antelopejs/interface-data-api/metadata";
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Access(AccessMode.ReadWrite)
@Validator((value) => typeof value === "string" && value.trim().length > 0)
declare name: string;
@Access(AccessMode.ReadWrite)
@Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
declare age: number;
}
Validation Function Signature
type FieldValidator = (
value: unknown,
) => FieldValidationResult | Promise<FieldValidationResult>;
type FieldValidationResult =
boolean | { success: true; data: unknown } | { success: false };
The function:
- Receives the raw field value from the request body.
- Returns
truewhen the value is valid: the raw value is written. - Returns
{ success: true, data }when the value is valid:datais written instead of the raw value. - Returns
falseor{ success: false }when the value is invalid, which triggers a400 Bad Requestresponse. - Should handle unexpected types gracefully (e.g., check
typeofbefore accessing properties).
Parsed Values
A validator returning a parse result writes the parsed value on both new and edit. The result has the shape of zod's safeParse, so a zod schema can be used directly:
const startsAtSchema = z.coerce.date();
@Access(AccessMode.ReadWrite)
@Validator((value) => startsAtSchema.safeParseAsync(value))
declare startsAt: Date; // "2026-01-15" is stored as a Date
Common Validation Patterns
String Validation
// Minimum length
@Validator((value) => typeof value === "string" && value.length >= 2)
declare firstName: string;
// Email format
const emailRegex = /^[\w](\.?[\w-]+)*@(?:[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$/;
@Validator((value) => typeof value === "string" && emailRegex.test(value))
declare email: string;
// Enum-like constraint
const VALID_ROLES = ["admin", "editor", "viewer"];
@Validator((value) => typeof value === "string" && VALID_ROLES.includes(value))
declare role: string;
Number Validation
// Range
@Validator((value) => typeof value === "number" && value >= 0 && value <= 120)
declare age: number;
// Integer only
@Validator((value) => typeof value === "number" && Number.isInteger(value))
declare quantity: number;
// Positive
@Validator((value) => typeof value === "number" && value > 0)
declare price: number;
Date Validation
// Valid date string
@Validator((value) => typeof value === "string" && !isNaN(Date.parse(value)))
declare birthDate: string;
// Future date only
@Validator((value) =>
typeof value === "string" &&
!isNaN(Date.parse(value)) &&
new Date(value) > new Date()
)
declare appointmentDate: string;
Async Validation
An asynchronous lookup can give early feedback, but a check followed by an insert is not a uniqueness guarantee: concurrent requests can both pass. Enforce uniqueness in the database where supported, and handle write conflicts. lookupByEmail below is an application-supplied function.
// Check uniqueness against the database
@Validator(async (value) => {
if (typeof value !== "string") return false;
const existing = await lookupByEmail(value);
return !existing;
})
declare email: string;
The @Mandatory Decorator
The @Mandatory decorator specifies which operations require a field in the request body. Unlike @Validator, which checks field content, @Mandatory only verifies that the field is provided:
- On
"edit", the body only carries the fields to update: a mandatory field absent from the body keeps its stored value and is accepted, while a mandatory field cleared by the body (null,""or[]) is rejected. - On any other operation (such as
"new"), a mandatory field must be present in the body.
import { Mandatory } from "@antelopejs/interface-data-api/metadata";
Usage
Pass one or more operation names as arguments. Common operation names are "new" (create) and "edit" (update).
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Access(AccessMode.ReadWrite)
@Mandatory("new", "edit")
declare email: string;
@Access(AccessMode.WriteOnly)
@Mandatory("new")
declare password: string;
@Access(AccessMode.ReadWrite)
@Mandatory("edit")
declare status: string;
@Access(AccessMode.ReadWrite)
declare notes: string; // Not mandatory for any operation
}
In this example:
emailmust be provided when creating a record and cannot be cleared when editing one.passwordis required only when creating a new record.statuscannot be cleared when editing an existing record.notesis always optional.
Skip Mandatory Validation
Use DefaultRoutes.WithOptions with the noMandatory option to create an endpoint that skips mandatory field checks.
const routes = {
edit: DefaultRoutes.Edit,
patchEdit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, {
noMandatory: "true",
}),
};
@RegisterDataController()
class UserAPI extends DataController(User, routes, Controller("/users")) {
@Access(AccessMode.ReadWrite)
@Mandatory("edit")
declare status: string;
}
Requests to /users/edit cannot clear status, while requests to /users/patchEdit can.
The @Optional Decorator
The @Optional decorator explicitly marks a field as not mandatory for any operation. Use it on fields that have no other decorators but still need to be recognized by the Data API metadata system.
import { Optional } from "@antelopejs/interface-data-api/metadata";
@Optional()
declare notes: string;
Error Handling
When validation or mandatory checks fail, the API returns a 400 Bad Request response with a plain text message describing the issue.
- Mandatory failure:
"Missing mandatory fields: email, password" - Validator failure:
"Invalid field type(s): age, email"
All fields are checked, and the error message lists every field that failed validation.
Next Steps
See the listable fields documentation to learn how to control field visibility in list operations and configure pagination.