JCC Express

The Basics

Validation

Introduction

Input validation uses validatorjs with JCC-specific async rules. Validate from the request with req.validate(rules, customMessages?), or use the global validate(rules, customMessages?) helper during a request.

When a controller action is injected with a FormRequest subclass, the container constructs it with the current req, then await requestInstance.rules() before your method runs. Implement async rules() and call await this.validate({ ... }) there so validation failures never reach the action (see Controllers.md and Request.md).

On failure, the framework flashes old input (the request body) and validation errors, then throws ValidationException. The error handler turns that into 422 responses: JSON for API-style requests, redirectBack() for typical browser forms.


Basic usage

Rules are an object: each key is a field name (matching req.body keys unless you use file rules), each value is a string or array of rule tokens (arrays work best with JCC’s normalizeBody for sometimes, nullable, file, and image).

TypeScript
await req.validate({
  email: ["required", "email"],
  password: ["required", "min:6"],
  confirm_password: ["required", "same:password"],
});

Custom messages (optional second argument):

TypeScript
await req.validate(
  {
    email: ["required", "email"],
  },
  {
    "required.email": "We need an email address.",
  }
);

After a successful run, req.validated is set to a function that returns the validated body (currently req.body). Validator.validate also resolves with that same object, so you can use either the return value or req.validated().

TypeScript
const data = await req.validate({ name: ["required"] });
// or, after await req.validate(...):
const again = req.validated!();

Use await on validate because the engine runs checkAsync. req.validated() itself is synchronous.


Global helper

TypeScript
await validate({
  title: ["required", "max:255"],
});

Same behavior as req.validate, but uses the current request from the container (only valid during an HTTP request).


Failed validation and the UI

When validation fails, Validator.validate flashes:

  • old — copy of req.body for repopulating forms.
  • VALIDATION_ERROR — error payload for templates.

HttpRequest maps the latest flash into res.locals.errors and res.locals.old on the next render so Blade can show field errors (see Request and JCC Blade). For JSON clients (expectsJson() and not Inertia), the framework responds with 422 and an errors payload.


Rule modifiers

JCC adjusts request data before rules run for these modifiers:

  • sometimes — If the field is missing from the body, that field can be skipped (optional presence).
  • nullable — Missing field becomes "" so nullable-style rules can run consistently.
  • file / image — Value is taken from uploaded files (req.hasFile, req.file) instead of raw body text.

Use these in the array form, for example:

TypeScript
await req.validate({
  avatar: ["sometimes", "image"],
  bio: ["nullable", "max:500"],
});

Supported validation rules

The project depends on validatorjs. JCC runs validation through checkAsync, so every rule name is resolved in an async pipeline. The framework also registers additional async rules for database and file checks (see JCC custom rules below).

Built-in rules from validatorjs

Presence and conditionals

  • required — Value is present and not empty after trimming whitespace.
  • sometimes — Always passes; combined with JCC normalizeBody, missing fields can be omitted from the normalized input so other rules are skipped for that key (see Rule modifiers in JCC).
  • present — Key must exist in input; value may be empty.
  • required_if:otherField,value — Required when otherField equals value.
  • required_unless:otherField,value — Required unless otherField equals value.
  • required_with:foo,bar,... — Required if any of the listed fields are present.
  • required_with_all:foo,bar,... — Required if all listed fields are present.
  • required_without:foo,bar,... — Required if any of the listed fields are absent.
  • required_without_all:foo,bar,... — Required if all listed fields are absent.

Strings and text shape

  • string — Value is a string.
  • email — Email-shaped string.
  • url — Valid URL format.
  • alpha — Letters only.
  • alpha_num — Letters and numbers only.
  • alpha_dash — Letters, numbers, dashes, underscores.
  • hex — Hexadecimal characters.
  • regex:pattern — Matches regex; prefer an array of rules if the pattern contains |. Escape backslashes for regex strings.

Numbers and digits

  • numeric — Numeric (string forms allowed).
  • integer — Integer value.
  • digits:n — Numeric string with exactly n digits.
  • digits_between:min,max — Digit length between min and max (see library behavior for empty values).

Size and bounds (length for strings, value for numbers; see library getSize)

  • min:n, max:n, size:n, between:min,max.

Sets and equality

  • in:foo,bar,... — Value must be one of the comma-separated options (also supports object/array rule form in raw validatorjs APIs).
  • not_in:foo,bar,... — Value must not be in the list.
  • same:otherField — Must equal otherField.
  • different:otherField — Must differ from otherField.
  • confirmed — Expects {field}_confirmation matching field (e.g. password / password_confirmation).

Other scalars

  • booleantrue / false / 0 / 1 and common string forms.
  • array — Value is an array.
  • acceptedyes, on, 1, or true (typical “accept terms” check).

Dates (parsed with JavaScript Date; library includes extra date sanity checks)

  • date
  • after:date, after_or_equal:date
  • before:date, before_or_equal:date

IP addresses

  • ip — IPv4 or IPv6.
  • ipv4, ipv6

Official behavior, edge cases, nested keys, and wildcards (e.g. users.*.email) are described in the validatorjs README on GitHub.

JCC custom rules

These are registered on every Validator.validate call alongside validatorjs:

  • nullable — Async rule that always calls passes(); meaningful use is together with normalizeBody, which injects "" when the field is missing so other rules can treat “empty” consistently.
  • unique:model or unique:model,column — No row may already have this value. model is resolved via getModelFile; column defaults to the attribute name. Implemented with existsHelper against your ORM (Lucid / Sequelize / Mongoose).
  • exists:model or exists:model,column — A row must exist for this value. Fails with “required” if the value is empty.
  • file — After normalization, the value reflects upload presence; fails if falsy (“The file field is required.”).
  • image — Same idea for image uploads (“The image field is required.”).

Use array rule lists for sometimes, nullable, file, and image so JCC’s normalizeBody can detect them (see Rule modifiers in JCC).


ValidationException

Thrown when checkAsync fails. The exception carries a flattened errors map (field → first message) for the handler. Validator.validate already flashes old and VALIDATION_ERROR before rejecting; the error handler may flash again when handling ValidationException for web flows. Do not catch and swallow the exception unless you intend to replace the default 422 / redirect behavior. For how these errors become HTTP responses (JSON vs redirectBack, status codes), see Error handling.


FormRequest example

TypeScript
import { FormRequest } from "jcc-express-mvc/Core/FormRequest";

export class StorePostRequest extends FormRequest {
  protected authorize(): boolean {
    return true;
  }

  protected async rules() {
    await this.validate({
      title: ["required", "max:255"],
      body: ["nullable"],
    });
  }
}

Use this.req.validated() only after this.validate(...) has completed successfully (for example inside the same class method). There is no this.validated() on FormRequest itself.

FormRequest messages

Override protected messages(): Record<string, string> on your request class. FormRequest.validate passes this.messages() as the second argument to req.validate, so custom keys follow validatorjs message conventions.

TypeScript
export class UserRequest extends FormRequest {
  protected messages(): Record<string, string> {
    return {
      "required.name": "Name is required.",   // applies only to the name field
      required: "This field is required.",     // applies to all required rules in this request
      "min.password": "Password must be at least 6 characters.",
    };
  }

  async rules() {
    await this.validate({
      name: ["required"],
      email: ["required", "email"],
      password: ["required", "min:6"],
    });
  }
}

Key format: "rule.field" targets one field; bare "rule" is a fallback for all fields that fail that rule.


Summary

  • Validate with await req.validate({ field: ["rule", ...] }, messages?) or await validate(...).
  • Prefer arrays of rules when using sometimes, nullable, file, and image.
  • unique / exists tie into your models; ValidationException produces 422 JSON or redirectBack with flashed old / errors.
  • FormRequest: implement async rules() with await this.validate({ ... }); the container runs rules() before your controller method when the request type is injected (Controllers.md). After success, use req.validated() or the validate return value (Request.md).