[data-reveal]{opacity:1!important;transform:none!important}
Validators
Data API

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 true when the value is valid: the raw value is written.
  • Returns { success: true, data } when the value is valid: data is written instead of the raw value.
  • Returns false or { success: false } when the value is invalid, which triggers a 400 Bad Request response.
  • Should handle unexpected types gracefully (e.g., check typeof before 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:

  • email must be provided when creating a record and cannot be cleared when editing one.
  • password is required only when creating a new record.
  • status cannot be cleared when editing an existing record.
  • notes is 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.