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 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;
}type FieldValue =
string | number | boolean | null | Date | FieldValue[] | FieldValues;
interface FieldValues {
[field: string]: FieldValue | undefined;
}
type FieldValidator = (
value: FieldValue | undefined,
) => FieldValidationResult | Promise<FieldValidationResult>;
type FieldValidationResult =
| boolean
| { success: true; data: FieldValue | undefined }
| { success: false };The function:
- Receives the raw field value from the request body: JSON, or a
Dateorundefinedwhen a route wrappingneworeditamended the parsed body.FieldValue | undefinedhas to be narrowed before use. - 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).
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// 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;// 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;// 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;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 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";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.
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 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;When validation or mandatory checks fail, the API returns a 400 Bad Request response whose JSON body names what went wrong and the fields concerned, for the client to word it and show it under each field:
- Mandatory failure:
{ "code": "missing_mandatory_fields", "fields": ["email", "password"] } - Validator failure:
{ "code": "invalid_field_types", "fields": ["age", "email"] }
The codes are exported as Validation.MISSING_MANDATORY_FIELDS and Validation.INVALID_FIELD_TYPES (@antelopejs/interface-data-api/components), the body's type as Validation.FieldsErrorBody. All fields are checked: fields lists every one that failed.
See the listable fields documentation to learn how to control field visibility in list operations and configure pagination.