
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!
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Building Reusable Validators in Angular Signal Forms
Learn how to create reusable custom validators with Angular Signal Forms, with practical examples for building flexible and maintainable form validation.
Alain Chautard
Sep 29, 2026

A Reference to Composable Patterns in Vue
A catalog of practical patterns for writing composables that are flexible, predictable, and easy to reuse.
Reza Baar
Sep 23, 2026

AbortController: Cancellation and Cleanup in JavaScript
AbortController does more than cancel a fetch. Learn how one signal cleans up event listeners, times out requests, and combines with AbortSignal.any().
Martin Ferret
Sep 22, 2026