
Learn how to create reusable custom validators with Angular Signal Forms, with practical examples for building flexible and maintainable form validation.
Alain Chautard
September 29, 2026
In this article, I will showcase how to create reusable custom validators with Angular Signal Forms. This content and these examples are excerpts from my book, Angular Signal Forms, available on both Amazon (paperback, Kindle) and Angular Training (PDF/EPUB). Let’s get started:
The Angular Signal Forms API provides several built-in validators:
Here is an example of such a built-in validator in action:
// A zip code has to be a 5-digit number
pattern(path.address.zip, /^\d{5}$/);
We can write custom validation rules as functions using the validate() helper. For instance, say we want to check that credit card numbers come from specific card providers, identified by their first digits:
import {SchemaPath, validate} from "@angular/forms/signals";
export function mustBeFromValidProvider(field: SchemaPath<string>) {
validate(field, ({value}) => {
if (!(value().startsWith('37') || value().startsWith('4') || value().startsWith('5'))) {
return {
kind: 'cc-provider',
message: 'Your credit card is not from a supported provider',
};
}
return null;
});
}
As illustrated in the above code, a validator function returns one of two things: — null, void, or undefined when no error is found - A ValidationError object with a kind and a message otherwise.
Once a custom validator function is defined, we can use it alongside our other validators:
// Credit card validation rules
required(path.cc, {message: 'Credit card number is required'});
minLength(path.cc, 16, {message: 'Credit card number must have 16 digits'});
maxLength(path.cc, 16, {message: 'Credit card number must be less than 17 digits'});
mustBeFromValidProvider(path.cc);
We can further improve this code for reusability. Assuming we have several forms using credit card numbers, it might make perfect sense to create our own function that runs all these checks at once:
import {maxLength, minLength, required,
SchemaPath, validate} from "@angular/forms/signals";
export function validateCreditCardNumber(field: SchemaPath<string>) {
required(field, {message: 'A credit card number is required'});
mustBeFromValidProvider(field);
minLength(field, 16, {message: 'A credit card number must be 16 digits long'});
maxLength(field, 16, {message: 'Credit card number must be less than 17 digits'});
}
And we can use the above function in our form setup for increased readability:
userForm = form(this.userInfo, (path) => {
required(path.firstName, {message: 'First name is required'});
required(path.address.zip);
validateCreditCardNumber(path.cc);
});

We just created a validateCreditCardNumber() function to group several validation rules into a single reusable helper. Signal Forms actually have a built-in primitive for this exact purpose: the schema() function.
import { schema, required, minLength } from '@angular/forms/signals';
interface Address {
street: string;
zip: string;
city: string;
country: string;
}
export const addressSchema = schema<Address>((address) => {
required(address.street, { message: 'Street is required' });
required(address.city, { message: 'City is required' });
required(address.zip, { message: 'Zip code is required' });
});
The type parameter (Address here) defines the shape of data the schema knows how to validate. Any form that matches this shape can use it.
To attach a reusable schema to a specific location in a form, we use the apply() function inside the form’s schema function:
import { apply, form, required } from '@angular/forms/signals';
import { addressSchema } from './address.schema';
userForm = form(this.userInfo, (path) => {
required(path.firstName, { message: 'First name is required' });
apply(path.address, addressSchema);
validateCreditCardNumber(path.cc);
});
When we call apply(), the schema receives a path scoped to that sub-object: inside addressSchema, address.zip refers to path.address.zip of whichever form the schema is applied to. This is what makes schemas truly reusable — a checkout form, a shipping form, and a user profile form can all apply() the same addressSchema to their own address group.
So when should you use a plain helper function like validateCreditCardNumber() versus schema()? Both are valid.
A helper function receives a path to a single field and works well for field-level rule bundles. A Schema
As a result, schemas tend to be more powerful and unlock additional possibilities.
This post and its examples are excerpts from my book, Angular Signal Forms, available on both Amazon (paperback, Kindle) and Angular Training (PDF/EPUB). Thanks for your support!
Ready to prove your Angular skills?
Get certified in AngularKeep learning Angular. Then get certified when you are ready.

How to create nested routes with Angular?
Learn how Angular nested routes and child routes work with multiple router outlets, and see how to use them to build navigable sections such as tabbed dashboards.
Alain Chautard
Sep 15, 2026

What’s new in Angular 22.1?
Angular 22.1 introduces a major change to Angular’s release cycle, moving to one major release per year. Learn what this means for Angular upgrades, versioning, and the future of the framework.
Alain Chautard
Aug 18, 2026

How to Add Async HTTP Validators to Angular Signal Forms
Learn how to implement asynchronous validation in Angular Signal Forms using validateHttp(). This guide shows how to validate user input with APIs for uniqueness checks, server-side rules, and other HTTP-based validation scenarios.
Alain Chautard
Jul 14, 2026