β X Validators
The simplest way to validate Flutter form fields β a small, composable set of rules
that plug straight into any TextFormField.validator.
- β Zero dependencies β pure Dart, nothing to ship but your code
- π§© Composable β stack rules; the first failure wins
- π Extensible β write your own rule by implementing one method
- π Localizable β translate every error message per rule type
- π§ͺ Well tested β every rule is covered by unit tests
Table of Contents
- Installation
- Quick start
- How it works
- API reference
- Examples for every rule
- Usage guide
- Good to know
- Migrating to 2.0.0
- License
π Installation
Add the dependency to your pubspec.yaml:
dependencies:
x_validators: ^2.0.0
Then run flutter pub get (or dart pub get).
import 'package:x_validators/x_validators.dart';
β‘ Quick start
xValidator takes a list of rules and returns a String? Function(String?) β
exactly the signature Flutter's form fields expect:
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: xValidator([
const IsRequired(),
const IsEmail(),
]),
);
The returned function yields null when the value is valid, or the error message
of the first failing rule otherwise.
π§ How it works
Every rule is a small class that extends TextXValidationRule and answers one
question β bool isValid(String input). xValidator runs the rules in order
and stops at the first one that fails:
xValidator([ ruleβ, ruleβ, ruleβ ]) βββΆ String? Function(String? value)
β
value ββΆ ruleβ.isValid? β yes ββΆ ruleβ.isValid? β yes ββΆ ruleβ ... ββΆ null (valid)
β no β no
βΌ βΌ
error message error message (first failure wins)
Because order matters, put broad rules first (e.g. IsRequired) and specific
rules after (e.g. IsEmail).
π API reference
Most rules accept an optional positional error message as their last
argument (e.g. IsRequired('This field is required')); a few (Match,
ContainsAny) take it as a named error parameter alongside their other named
options. When omitted, the rule's default message is used (see
Localizing error messages).
Text
| Rule | Description |
|---|---|
IsRequired([error]) |
Fails when the trimmed input is empty. |
IsEmpty([error]) |
Passes only when the trimmed input is empty. |
Contains(value, [error]) |
Input contains value (compared after trimming). |
NotContains(value, [error]) |
Input does not contain value. |
StartsWith(prefix, [error]) |
Input starts with prefix. |
EndsWith(suffix, [error]) |
Input ends with suffix. |
Match(value, {caseSensitive = true}, [error]) |
Input equals value; case-insensitive when caseSensitive is false. |
MinLength(min, [error]) |
Trimmed length is >= min. |
MaxLength(max, [error]) |
Trimmed length is <= max. |
Numbers
| Rule | Description |
|---|---|
IsNumber([error]) |
Input is an integer (use IsDecimal for fractional values). |
IsDecimal([error]) |
Input is a decimal number (integers are accepted too). |
IsArabicNum([error]) |
Positive integer written in Latin digits 0-9 (no leading zero). |
IsHindiNum([error]) |
Number written in Arabic-Indic digits Ω -Ω©. |
MinValue(min, [error]) |
Parsed numeric value is >= min. |
MaxValue(max, [error]) |
Parsed numeric value is <= max. |
URLs
| Rule | Description |
|---|---|
IsUrl([error]) |
Input is a valid http/https URL. |
IsSecureUrl([error]) |
Input is a URL using the https:// scheme. |
IsFacebookUrl([error]) |
Input is a valid Facebook URL. |
IsInstagramUrl([error]) |
Input is a valid Instagram URL. |
IsYoutubeUrl([error]) |
Input is a valid YouTube URL. |
Phone
| Rule | Description |
|---|---|
IsEgyptianPhone([error]) |
Input is a valid Egyptian phone number. |
ISKsaPhone([error]) |
Input is a valid Saudi Arabian phone number. |
IT
| Rule | Description |
|---|---|
IsEmail([error]) |
Input is a valid email address. |
IsBool([error]) |
Input is a valid boolean value. |
IsIpAddress([error]) |
Input is a valid IP address. |
IsPort([error]) |
Input is a valid port number. |
RegExpRule(regExp, [error]) |
Input matches the provided RegExp. |
Lists
| Rule | Description |
|---|---|
IsIn(values, [error]) |
Input is one of the values in the provided list. |
IsNotIn(values, [error]) |
Input is not in the provided list. |
ContainsAny(values, {caseSensitive = false, error}) |
Input contains at least one item from the list (case-insensitive by default). |
NotContainsAny(values, [error]) |
Input contains none of the items in the list. |
Dates
| Rule | Description |
|---|---|
IsDate([error]) |
Input is a parseable date string. |
IsDateMillis([error]) |
Input is an integer of milliseconds since epoch. |
IsDateAfter(date, [error]) |
Input is a date later than date. |
Languages
| Rule | Description |
|---|---|
IsArabicChars([error]) |
Input consists of Arabic letters, whitespace and Arabic-Indic digits Ω -Ω©. |
IsEnglishChars([error]) |
Only English letters AβZ (no spaces or digits). |
IsNumbersOnly([error]) |
Input is all digits (one or more). |
IsLtrLanguage([error]) |
Input is a left-to-right language code. |
IsRTLLanguage([error]) |
Input is a right-to-left language code. |
Colors
| Rule | Description |
|---|---|
IsHexColor([error]) |
Input is a valid 3/6/8-digit hex color. |
Magic
| Rule | Description |
|---|---|
IsOptional() |
When present, an empty field skips all other rules and passes. |
Examples for every rule
Every rule is shown below with a passing and a failing input. The examples use
rule.isValid(value) so you can see the boolean directly; inside a form you'd
normally pass the rule to xValidator([...]) and get the error message instead
(see Composing rules). Each rule also has a matching
top-level function (isEmail, isHexColor, β¦) for one-off checks β see
Standalone helper functions.
Text
const IsRequired().isValid('Jane'); // β true
const IsRequired().isValid(' '); // β false (trimmed empty)
const IsEmpty().isValid(' '); // β true
const IsEmpty().isValid('x'); // β false
const Contains('@').isValid('a@b.com'); // β true
const Contains('@').isValid('abc'); // β false
const NotContains(' ').isValid('no_spaces'); // β true
const NotContains(' ').isValid('has space'); // β false
const StartsWith('+20').isValid('+20100'); // β true
const StartsWith('+20').isValid('0100'); // β false
const EndsWith('.com').isValid('site.com'); // β true
const EndsWith('.com').isValid('site.org'); // β false
const Match('Yes').isValid('Yes'); // β true (case-sensitive by default)
const Match('Yes').isValid('yes'); // β false
const Match('Yes', caseSensitive: false).isValid('yes'); // β true
const MinLength(3).isValid('abc'); // β true
const MinLength(3).isValid('ab'); // β false
const MaxLength(5).isValid('abcde'); // β true
const MaxLength(5).isValid('abcdef'); // β false
Numbers
const IsNumber().isValid('42'); // β true
const IsNumber().isValid('-7'); // β true
const IsNumber().isValid('3.14'); // β false (use IsDecimal)
const IsNumber().isValid('0x1A'); // β false (no hex)
const IsDecimal().isValid('3.14'); // β true
const IsDecimal().isValid('42'); // β true (integers too)
const IsDecimal().isValid('abc'); // β false
const IsArabicNum().isValid('123'); // β true (Latin digits, positive)
const IsArabicNum().isValid('012'); // β false (leading zero)
const IsArabicNum().isValid('0'); // β false
const IsHindiNum().isValid('Ω‘Ω’Ω£'); // β true (Arabic-Indic digits)
const IsHindiNum().isValid('Ω Ω‘Ω’'); // β false (leading Ω )
const IsHindiNum().isValid('123'); // β false (Latin digits)
const MinValue(18).isValid('18'); // β true
const MinValue(18).isValid('17'); // β false
const MaxValue(100).isValid('100'); // β true
const MaxValue(100).isValid('101'); // β false
URLs
const IsUrl().isValid('https://my-site.co.uk'); // β true
const IsUrl().isValid('https://a.b.example.com/p?x=1'); // β true
const IsUrl().isValid('example.com'); // β false (no scheme)
const IsSecureUrl().isValid('https://x.com'); // β true
const IsSecureUrl().isValid('http://x.com'); // β false
const IsFacebookUrl().isValid('https://facebook.com/me'); // β true
const IsFacebookUrl().isValid('https://facebook.com.evil.com'); // β false
const IsInstagramUrl().isValid('https://instagram.com/me'); // β true
const IsInstagramUrl().isValid('https://evil.com'); // β false
const IsYoutubeUrl().isValid('https://youtube.com/watch?v=x'); // β true
const IsYoutubeUrl().isValid('https://vimeo.com/1'); // β false
Phone
const IsEgyptianPhone().isValid('01012345678'); // β true (010/011/012/015 + 8 digits)
const IsEgyptianPhone().isValid('01312345678'); // β false (013 is not a valid prefix)
const ISKsaPhone().isValid('0512345678'); // β true
const ISKsaPhone().isValid('+966512345678'); // β true
const ISKsaPhone().isValid('0612345678'); // β false
IT
const IsEmail().isValid('jane.doe@example.com'); // β true
const IsEmail().isValid('plainaddress'); // β false
const IsBool().isValid('true'); // β true
const IsBool().isValid(' FALSE '); // β true (trimmed, case-insensitive)
const IsBool().isValid('yes'); // β false
const IsIpAddress().isValid('192.168.1.1'); // β true
const IsIpAddress().isValid('192.168.001.001'); // β false (leading zeros)
const IsPort().isValid('8080'); // β true (0β65535)
const IsPort().isValid('65536'); // β false
RegExpRule(RegExp(r'^[A-Z]{3}$')).isValid('EGP'); // β true
RegExpRule(RegExp(r'^[A-Z]{3}$')).isValid('usd'); // β false
Lists
const IsIn(['red', 'green', 'blue']).isValid('green'); // β true
const IsIn(['red', 'green', 'blue']).isValid('yellow'); // β false
const IsNotIn(['admin', 'root']).isValid('guest'); // β true
const IsNotIn(['admin', 'root']).isValid('admin'); // β false
const ContainsAny(['http', 'https']).isValid('https://x'); // β true
const ContainsAny(['http', 'https']).isValid('ftp://x'); // β false
const ContainsAny(['USD']).isValid('usd ok'); // β true (case-insensitive)
const ContainsAny(['USD'], caseSensitive: true).isValid('usd ok'); // β false
const NotContainsAny(['<', '>']).isValid('safe'); // β true
const NotContainsAny(['<', '>']).isValid('a<b'); // β false
Dates
const IsDate().isValid('2026-06-01'); // β true
const IsDate().isValid('not-a-date'); // β false
const IsDateMillis().isValid('1700000000000'); // β true
const IsDateMillis().isValid('3.14'); // β false
IsDateAfter(DateTime(2020)).isValid('2026-01-01'); // β true
IsDateAfter(DateTime(2020)).isValid('2019-01-01'); // β false
Languages
const IsArabicChars().isValid('Ω
Ψ±ΨΨ¨Ψ§ Ψ¨Ω'); // β true
const IsArabicChars().isValid('Ω
Ψ±ΨΨ¨Ψ§ Ω‘Ω’Ω£'); // β true (Arabic-Indic digits allowed)
const IsArabicChars().isValid('hello'); // β false
const IsEnglishChars().isValid('Hello'); // β true
const IsEnglishChars().isValid('Hello World'); // β false (no spaces)
const IsEnglishChars().isValid('abc123'); // β false (letters only)
const IsNumbersOnly().isValid('12345'); // β true
const IsNumbersOnly().isValid('12a'); // β false
const IsLtrLanguage().isValid('en'); // β true
const IsLtrLanguage().isValid('ar'); // β false
const IsRTLLanguage().isValid('ar'); // β true
const IsRTLLanguage().isValid('en'); // β false
Colors
const IsHexColor().isValid('#FFF'); // β true (3 digits)
const IsHexColor().isValid('A1B2C3'); // β true (6 digits, '#' optional)
const IsHexColor().isValid('#FF0000FF'); // β true (8 digits, with alpha)
const IsHexColor().isValid('red'); // β false
Magic β IsOptional
IsOptional only makes sense inside a validator: an empty value skips the rest
and passes, but a non-empty value still has to satisfy them.
final validate = xValidator([
const IsOptional(),
const IsEmail(),
]);
validate(''); // β null (empty is allowed)
validate('a@b.co'); // β null (valid email)
validate('x'); // β IsEmail's message (non-empty must be valid)
π Usage guide
Composing rules
Rules run top-to-bottom and the first failure is returned, so order them from general to specific:
final validate = xValidator([
const IsRequired(), // checked first
const MinLength(8),
const MaxLength(32),
]);
validate(''); // β IsRequired's message
validate('abc'); // β MinLength's message
validate('hunter2!'); // β null (valid)
Real-world recipes
Ready-to-paste validators for the fields you actually build. Each returns the
first failing rule's message, or null when the value passes β exactly what
TextFormField.validator expects.
Password β required, length-bounded, must contain a special character:
final password = xValidator([
const IsRequired('Password is required'),
const MinLength(8, 'At least 8 characters'),
const MaxLength(64, 'At most 64 characters'),
const ContainsAny(['!', '@', '#', '%'], error: 'Add a special character'),
]);
password(''); // β 'Password is required'
password('abc'); // β 'At least 8 characters'
password('abcdefgh'); // β 'Add a special character'
password('abcdefg!'); // β null (valid)
Email β required and well-formed:
final email = xValidator([
const IsRequired('Email is required'),
const IsEmail('Enter a valid email'),
]);
email(''); // β 'Email is required'
email('not-an-email'); // β 'Enter a valid email'
email('jane@acme.io'); // β null (valid)
Phone β required Egyptian mobile number:
final phone = xValidator([
const IsRequired('Phone is required'),
const IsEgyptianPhone('Enter a valid Egyptian number'),
]);
phone('0100'); // β 'Enter a valid Egyptian number'
phone('01012345678'); // β null (valid)
Optional age β blank is allowed, but if filled it must be a whole number in
range. Put IsOptional first so an empty value skips the rest and passes:
final age = xValidator([
const IsOptional(),
const IsNumber('Digits only'),
const MinValue(18, 'Must be 18 or older'),
const MaxValue(120, 'Enter a real age'),
]);
age(''); // β null (optional β blank is fine)
age('1x'); // β 'Digits only'
age('16'); // β 'Must be 18 or older'
age('30'); // β null (valid)
Optional website β blank allowed, otherwise a valid secure URL:
final website = xValidator([
const IsOptional(),
const IsUrl('Enter a valid URL'),
const IsSecureUrl('Use https://'),
]);
website(''); // β null (optional β blank is fine)
website('not a url'); // β 'Enter a valid URL'
website('http://insecure.io'); // β 'Use https://'
website('https://acme.io'); // β null (valid)
Custom error messages
Pass a message as the rule's last argument to override its default:
xValidator([
const IsRequired('Please enter a value'),
const MinLength(8, 'Use at least 8 characters'),
]);
Reacting to failures (onFailureCallBack)
Useful for analytics or logging. The callback receives the input, the full rule list, and the rule that failed:
xValidator(
[
const IsRequired('Field cannot be empty'),
const MinLength(3, 'At least 3 characters'),
const MaxLength(20, 'At most 20 characters'),
],
onFailureCallBack: (input, rules, failedRule) {
debugPrint('Validation failed for "$input" on ${failedRule.runtimeType}');
},
);
Optional fields
Add IsOptional to let an empty field pass while still validating non-empty
input. Here an empty value is accepted, but anything typed must be a valid email:
xValidator([
const IsOptional(),
const IsEmail(),
]);
Writing a custom rule
Extend TextXValidationRule, implement isValid, and (optionally) override
defaultMessage to provide a default message:
class StartsWithCapital extends TextXValidationRule {
const StartsWithCapital([super.error]);
@override
bool isValid(String input) =>
input.isNotEmpty && input[0] == input[0].toUpperCase();
@override
String get defaultMessage => 'Must start with a capital letter';
}
// Use it like any built-in rule:
xValidator([const StartsWithCapital()]);
Overriding
toString()still works β the basedefaultMessagedelegates to it β butdefaultMessageis the preferred hook for a rule's default message.
Localizing error messages
Register a translator per rule type with XValidatorsLocalization.on<T>().
When that rule fails (and no inline error was given), your function supplies
the message:
XValidatorsLocalization.on<IsRequired>((rule) => 'ΩΨ°Ψ§ Ψ§ΩΨΩΩ Ω
Ψ·ΩΩΨ¨');
XValidatorsLocalization.on<IsEmail>((rule) => 'Ψ§ΩΨ¨Ψ±ΩΨ― Ψ§ΩΨ₯ΩΩΨͺΨ±ΩΩΩ ΨΊΩΨ± Ψ΅Ψ§ΩΨ');
final validate = xValidator([const IsRequired()]);
validate(''); // β 'ΩΨ°Ψ§ Ψ§ΩΨΩΩ Ω
Ψ·ΩΩΨ¨'
Resolution order for a failing rule is: inline error β registered
translator β rule.defaultMessage.
Standalone helper functions
Each rule also ships a top-level function for one-off checks outside of a form, mirroring the rule's logic:
isNotEmpty('hello'); // true
isEmpty(' '); // true
minLength('abc', 3); // true
EmailXValidator.validate('test@example.com'); // true
βΉοΈ Good to know
- A
nullvalue passed to the validator is treated as an empty string'', soIsRequired(and every other rule) sees''and a required field correctly fails onnull. Add anIsOptionalrule if you want an empty/nullvalue to skip the remaining rules and pass. (Before 2.0.0,nullshort-circuited the whole validator to "valid" β see Migrating to 2.0.0.)
Migrating to 2.0.0
2.0.0 is a behavior-only major release: the public API shape is unchanged (same
rules, same xValidator signature), but several rules were tightened to do what
their names promise. Most apps need no code changes β the table below lists every
behavior change and how to restore the old behavior where it's recoverable.
| Area | Before (1.x) | After (2.0.0) | How to adapt |
|---|---|---|---|
null input |
Short-circuited the whole validator to valid | Treated as '', so IsRequired rejects it |
Add IsOptional to let empty/null pass |
IsNumber |
Accepted decimals, hex, scientific (3.14, 1e3, 0x1A) |
Integers only | Use the new IsDecimal for fractional values |
IsArabicChars |
\p{N} token matched the literal chars p{N} and no digits |
Accepts Arabic-Indic digits Ω -Ω© only |
β (this was a bug; Latin 123 was never really allowed) |
IsNumbersOnly |
Passed if the input contained a digit ('abc1', '12 34') |
Passes only when the input is all digits | Use Contains/RegExpRule for "contains a digit" |
IsFacebookUrl / IsInstagramUrl / IsYoutubeUrl |
Unanchored β facebook.com.evil.com passed |
Fully anchored β domain-suffix spoofing is rejected | β (intended hardening) |
IsUrl |
Rejected hyphens, deep subdomains, long TLDs | Accepts my-site.co.uk, a.b.example.com, example.museum |
β (relaxation only) |
IsIpAddress |
Tolerated leading zeros and whitespace (192.168.001.001, ' 1.2.3.4') |
Strict dotted-quad | Trim/normalize before validating if needed |
ContainsAny |
error was positional; caseSensitive was a dead no-op |
error is named; caseSensitive works |
See snippet below |
IsArabicNum / IsHindiNum |
Both used key validation.must_be_num |
validation.must_be_arabic_num / validation.must_be_hindi_num |
Re-key your translators (inline error: is unaffected) |
isInstgramUrlValid |
Typo'd free function | Renamed to isInstagramUrlValid |
Old name still works as a @Deprecated alias |
Decimals: IsNumber β IsDecimal
// 1.x β accepted "3.14"
xValidator([const IsNumber()]);
// 2.0.0 β integers only; use IsDecimal for fractional input
xValidator([const IsDecimal()]);
ContainsAny: error is now named, caseSensitive now works
// 1.x
const ContainsAny(['a', 'b'], 'Pick one'); // positional error
final r = ContainsAny(['a'])..caseSensitive = true; // dead no-op field
// 2.0.0
const ContainsAny(['a', 'b'], error: 'Pick one'); // named error
const ContainsAny(['a'], caseSensitive: true); // actually case-sensitive
If you imported individual rule files directly, note two internal renames:
text/is_not_empty.dartβtext/is_required.dartandurls/is_instgram_url.dartβurls/is_instagram_url.dart. Importing the package barrel (package:x_validators/x_validators.dart) needs no change.
π License
See LICENSE.
π¨π»βπ» Author
|
Mahmoud Basuony Software Engineer If x_validators saved you some boilerplate, a β on the repo is appreciated. |
Libraries
- core/utils/language_utils
- email_validator
- rules/colors/is_hex_color
- rules/dates/date_after
- rules/dates/is_date
- rules/dates/is_date_mills
- rules/index
- A library that provides a collection of text validation rules for various purposes.
- rules/it/is_boolean
- rules/it/is_email
- rules/it/is_ip_address
- rules/it/is_port
- rules/it/regx
- rules/languages/is_arabic_chars
- rules/languages/is_english_char
- rules/languages/is_ltr_language
- rules/languages/is_number_only
- rules/languages/is_rtl_language
- rules/lists/contains_any
- rules/lists/is_in
- rules/lists/is_not_in
- rules/lists/not_contains_any
- rules/magic/is_optional
- rules/numbers/is_arabic_num
- rules/numbers/is_decimal
- rules/numbers/is_hindi_num
- rules/numbers/is_number
- rules/numbers/max_value
- rules/numbers/min_value
- rules/phone/is_egy_number
- rules/phone/is_ksa_number
- rules/text/contains
- rules/text/ends_with
- rules/text/is_empty
- rules/text/is_required
- rules/text/match
- rules/text/max_length
- rules/text/min_length
- rules/text/not_contains
- rules/text/starts_with
- rules/urls/is_facebook_url
- rules/urls/is_instagram_url
- rules/urls/is_secure_url
- rules/urls/is_url
- rules/urls/is_youtube_url
- text_rule_class
- tr
- validator
- x_validators
- A library that provides a collection of text validation utilities.
