NAME
Validator::LIVR - Lightweight validator supporting Language Independent Validation Rules Specification (LIVR)
SYNOPSIS
use Validator::LIVR;
my $validator = Validator::LIVR->new({
name => 'required',
email => [ 'required', 'email' ],
gender => { one_of => [ 'male', 'female' ] },
phone => { max_length => 10 },
password => [ 'required', { min_length => 10 } ],
password2 => { equal_to_field => 'password' },
});
if ( my $valid_data = $validator->validate($user_data) ) {
save_user($valid_data);
} else {
my $errors = $validator->get_errors();
# { email => 'WRONG_EMAIL', password => 'TOO_SHORT' }
}
DESCRIPTION
LIVR rules are plain data: a hash that maps field names to rules. They are easy to read, and because the same rules work in a dozen languages, one rule set can check a form in the browser with JavaScript and again on the server with Perl.
This module implements LIVR 2.0 and passes the test suite of the specification.
Declarative rules that you can keep in JSON and share with other services.
All errors at once, not just the first one.
Only described fields in the output. A field without rules, such as an unexpected
is_admin, never reaches your code.Nested data: objects, lists, lists of objects and objects of several types.
Error codes, not messages, such as
REQUIREDorTOO_SHORT. Turn them into messages in any language you need.Data cleanup with modifiers, such as
trim,to_lcanddefault.Your own rules, as aliases of existing rules or as Perl code.
No dependencies: only modules that come with Perl.
Use it for user input, configs, API requests and responses, or any other data that has to have a known shape.
A QUICK TOUR
Writing rules
A rule set maps each field to its rules. A rule is written in one of these ways:
{
name => 'required', # a rule without arguments
bio => { max_length => 1000 }, # a rule with one argument
nick => { length_between => [ 3, 20 ] }, # a rule with several arguments
email => [ 'required', 'email' ], # several rules
}
Several rules of a field are applied in order, and the field stops at its first error. A hash is always one rule, so two rules with arguments are two hashes: [ { min_length => 3 }, { max_length => 20 } ].
Most rules accept empty values: undef and the empty string. So a field is optional until you add required:
{
age => 'positive_integer', # can be missing
id => [ 'required', 'positive_integer' ], # must be present
}
Validating nested data
Metarules describe the structure of the data, and errors have the same shape as the data:
my $validator = Validator::LIVR->new({
order_id => [ 'required', 'positive_integer' ],
address => [ 'required', { nested_object => {
country => [ 'required', { one_of => [ 'Ukraine', 'USA' ] } ],
city => 'required',
zip => 'positive_integer',
} } ],
products => [ 'required', 'not_empty_list', { list_of_objects => {
product_id => [ 'required', 'positive_integer' ],
quantity => [ 'required', { min_number => 1 } ],
} } ],
tags => { list_of => [ 'string', { max_length => 10 } ] },
});
For an order with an unknown country, a second product with a quantity of 0 and a second tag that is too long, get_errors returns:
{
address => { country => 'NOT_ALLOWED_VALUE' },
products => [ undef, { quantity => 'TOO_LOW' } ],
tags => [ undef, 'TOO_LONG' ],
}
Cleaning data
Modifiers change the value for the next rules and for the output:
my $validator = Validator::LIVR->new({
email => [ 'trim', 'required', 'email', 'to_lc' ],
phone => { leave_only => '0123456789' },
role => [ { default => 'user' }, { one_of => [ 'user', 'admin' ] } ],
});
$validator->validate({ email => ' John@Example.COM ', phone => '+38 (050) 123-45-67' });
# { email => 'john@example.com', phone => '380501234567', role => 'user' }
To trim every string of the input before validation, turn on auto trim for one validator with Validator::LIVR->new( $rules, 1 ), or for all new validators with Validator::LIVR->default_auto_trim(1).
Sharing rules with other languages
Rules are plain data, so they can live in a JSON file:
{
"email": [ "required", "email" ],
"password": [ "required", { "min_length": 10 } ]
}
use JSON::PP qw(decode_json);
my $validator = Validator::LIVR->new( decode_json($json_rules) );
The same file works with livr in JavaScript, and with the implementations for PHP, Python, Go, Java, Ruby, Erlang, Lua, Raku and Swift. You can try rules in the playground.
Adding your own rules
The simplest way is an alias: a new name for a combination of rules, with its own error code. Aliases are a part of the specification, so other implementations understand them too.
Validator::LIVR->register_aliased_default_rule({
name => 'adult_age',
rules => [ 'positive_integer', { min_number => 18 } ],
error => 'WRONG_AGE',
});
my $validator = Validator::LIVR->new({ age => [ 'required', 'adult_age' ] });
For anything else, write the rule in Perl, see "WRITING RULES IN PERL".
RULES
The built-in rules, grouped as in the specification. Each group links to its page in the specification, which has the details and edge cases.
A rule that expects a string or a number returns FORMAT_ERROR for a hash or a list. A rule that expects a hash or a list returns FORMAT_ERROR for anything else.
Common rules
required-
The field is present and not empty: not
undefand not''. Error:REQUIRED. not_empty-
The field can be missing, but when it is there, it is not
''. Error:CANNOT_BE_EMPTY. not_empty_list-
The field is a list with at least one item. Errors:
CANNOT_BE_EMPTY,FORMAT_ERROR. any_object-
The field is a hash with any content. Error:
FORMAT_ERROR.
String rules
string-
Any string or number. Error:
FORMAT_ERROR. { eq => $value }-
The value is equal to
$value. Error:NOT_ALLOWED_VALUE. { one_of => [ @values ] }-
The value is equal to one of
@values. Error:NOT_ALLOWED_VALUE. { min_length => $length }-
The value has at least
$lengthcharacters. Error:TOO_SHORT. { max_length => $length }-
The value has at most
$lengthcharacters. Error:TOO_LONG. { length_equal => $length }-
The value has exactly
$lengthcharacters. Errors:TOO_SHORT,TOO_LONG. { length_between => [ $min, $max ] }-
The value has from
$minto$maxcharacters. Errors:TOO_SHORT,TOO_LONG. { like => $regex },{ like => [ $regex, 'i' ] }-
The value matches the regular expression. The
iflag makes it case insensitive. Error:WRONG_FORMAT.The rule is experimental in the specification. The syntax of regular expressions differs between languages, so stick to the common features when you share rules.
Numeric rules
integer-
An integer, such as
-5or42. Error:NOT_INTEGER. positive_integer-
An integer greater than zero. Error:
NOT_POSITIVE_INTEGER. decimal-
A number, such as
-1.5or42. Error:NOT_DECIMAL. positive_decimal-
A number greater than zero. Error:
NOT_POSITIVE_DECIMAL. { min_number => $min }-
A number that is not less than
$min. Errors:NOT_NUMBER,TOO_LOW. { max_number => $max }-
A number that is not greater than
$max. Errors:NOT_NUMBER,TOO_HIGH. { number_between => [ $min, $max ] }-
A number from
$minto$max. Errors:NOT_NUMBER,TOO_LOW,TOO_HIGH.
Special rules
email-
An email address, such as
john.smith@example.com, without a name, comments or spaces around it. The domain has a TLD or is an IP address in brackets. Error:WRONG_EMAIL. url-
An
httporhttpsURL, such ashttps://example.com/path?query#fragment. The host is a domain name, such asexample.comorintranet, or an IPv4 address. Domain names in other languages work in decoded strings, not in UTF-8 bytes. Error:WRONG_URL. iso_date-
A date as
YYYY-MM-DDthat exists in the calendar:2024-02-29is valid,2025-02-29is not. Error:WRONG_DATE. { equal_to_field => $field }-
The value is equal to the value of another field, such as a password confirmation. Error:
FIELDS_NOT_EQUAL.
Metarules
Metarules validate nested data with their own rule sets. Their errors are nested too: a hash of errors for an object, and a list for a list, with undef for the valid items.
{ nested_object => \%rules }-
A hash validated with
\%rules. { list_of => \@rules },{ list_of => $rule }-
A list, each item validated with the rules.
{ list_of_objects => \%rules }-
A list of hashes, each validated with
\%rules. { variable_object => [ $selector, { $type => \%rules, ... } ] }-
A hash validated with the rules for the value of its
$selectorfield. The error isFORMAT_ERRORwhen the hash has no rules for that value.payment => [ 'required', { variable_object => [ type => { card => { type => 'required', card_number => [ 'required', { length_equal => 16 } ], }, iban => { type => 'required', iban => 'required', }, } ] } ], { list_of_different_objects => [ $selector, { $type => \%rules, ... } ] }-
A list of hashes, each validated as in
variable_object. { or => [ $rules1, $rules2, ... ] }-
The value passes at least one of the rule sets. Each rule set is a rule or a list of rules. When all of them fail, the error is the error of the last one.
id => { or => [ 'email', 'positive_integer' ] },
Modifiers
Modifiers change the value and never fail.
trim-
Removes the spaces at the start and at the end.
to_lc-
Converts to lower case.
to_uc-
Converts to upper case.
{ remove => $chars }-
Removes each of the characters in
$chars, so{ remove => ' -()' }turns(050) 123-45-67into0501234567. { leave_only => $chars }-
Removes all characters except the ones in
$chars. { default => $value }-
Sets
$valuewhen the field is missing or empty.
CLASS METHODS
new
my $validator = Validator::LIVR->new( \%rules );
my $validator = Validator::LIVR->new( \%rules, $is_auto_trim );
Creates a validator. With a true $is_auto_trim, it trims all strings of the input, the nested ones too, before validation, so the output is trimmed as well. When $is_auto_trim is undef, the value set with "default_auto_trim" is used.
register_default_rules
Validator::LIVR->register_default_rules(
my_rule => \&my_rule_builder,
other_rule => \&other_rule_builder,
);
Adds rules or replaces existing ones for all validators created after the call. See "WRITING RULES IN PERL" for how to write a rule builder.
register_aliased_default_rule
Validator::LIVR->register_aliased_default_rule({
name => 'valid_address',
rules => { nested_object => {
country => 'required',
city => 'required',
zip => 'positive_integer',
} },
error => 'WRONG_ADDRESS', # optional
});
Adds an alias for all validators created after the call. Then the alias works as any other rule:
{ address => [ 'required', 'valid_address' ] }
Without error, the alias returns the error of its rules.
get_default_rules
my $rule_builders = Validator::LIVR->get_default_rules();
Returns a hashref that maps rule names to the rule builders that new validators get.
default_auto_trim
Validator::LIVR->default_auto_trim(1);
Turns auto trim on or off for validators created after the call. See "new".
OBJECT METHODS
validate
my $valid_data = $validator->validate( \%input );
Returns a hashref with the valid data: only the fields that have rules, with the changes made by modifiers. On errors returns undef, and "get_errors" returns the errors.
get_errors
my $errors = $validator->get_errors();
Returns the errors of the last "validate" call, or undef when it passed. The errors are a hashref that maps fields to error codes, with nested errors for metarules:
{
email => 'WRONG_EMAIL',
address => { zip => 'NOT_POSITIVE_INTEGER' },
products => [ undef, { quantity => 'TOO_LOW' } ],
}
When the input is not a hashref, the errors are the string FORMAT_ERROR.
register_rules
$validator->register_rules( my_rule => \&my_rule_builder, ... );
Adds rules or replaces existing ones, as "register_default_rules" does, for this validator only. Returns the validator, so the call can be chained.
register_aliased_rule
$validator->register_aliased_rule({
name => 'strong_password',
rules => { min_length => 12 },
error => 'WEAK_PASSWORD',
});
Adds an alias, as "register_aliased_default_rule" does, for this validator only. Returns the validator.
get_rules
my $rule_builders = $validator->get_rules();
Returns a hashref that maps rule names to the rule builders of this validator.
prepare
my $validator = Validator::LIVR->new($rules)->prepare();
Builds the validators of all rules and returns the validator. "validate" does it on the first call, so you need prepare only to find mistakes early. It dies with Rule [name] not registered when a rule is unknown, and with Rule [name1, name2] should have one name when a hash has several rules. Register your rules before prepare or the first "validate".
WRITING RULES IN PERL
A rule is a builder: a sub that gets the arguments of the rule and returns a validator sub. The builder is called once, when the validator is prepared. The validator sub is called for every value.
Here is how max_number could be written:
use Scalar::Util qw(looks_like_number);
Validator::LIVR->register_default_rules( max_number => sub {
my ( $max_number, $rule_builders ) = @_;
return sub {
my ( $value, $all_values, $output_ref ) = @_;
# Empty values are the job of "required"
return if !defined($value) || $value eq '';
return 'FORMAT_ERROR' if ref($value);
return 'NOT_NUMBER' if !looks_like_number($value);
return 'TOO_HIGH' if $value > $max_number;
return;
};
});
The builder gets the arguments of the rule, then a hashref of all rule builders of the validator:
'my_rule' # ( $rule_builders )
{ my_rule => $arg } # ( $arg, $rule_builders )
{ my_rule => [ $arg ] } # ( $arg, $rule_builders )
{ my_rule => [ $arg1, $arg2 ] } # ( $arg1, $arg2, $rule_builders )
The validator sub gets:
$value-
The value of the field, as the previous rules of the field left it.
$all_values-
The hash that has the field, to compare it with other fields, as
equal_to_fielddoes. Insidelist_of,orand aliases, rules check the value on its own, so this hash has no other fields. $output_ref-
A reference to the output value. Assign to it to change the value, as modifiers do.
It returns nothing when the value is valid, and an error code when it is not.
A modifier sets the output and returns nothing:
Validator::LIVR->register_default_rules( to_ucfirst => sub {
return sub {
my ( $value, undef, $output_ref ) = @_;
return if !defined($value) || ref($value) || $value eq '';
$$output_ref = ucfirst($value);
return;
};
});
With $rule_builders, a rule can validate nested data with the same rules, as metarules do:
my $validator = Validator::LIVR->new($rules)->register_rules(%$rule_builders)->prepare();
The built-in rules are short and make good examples: Validator::LIVR::Rules::Common, Validator::LIVR::Rules::String, Validator::LIVR::Rules::Numeric, Validator::LIVR::Rules::Special, Validator::LIVR::Rules::Meta and Validator::LIVR::Rules::Modifiers.
SEE ALSO
The LIVR specification, with all rules, aliases and a playground.
The LIVR repository, with the specification and its test suite.
LIVR implementations in other languages.
BUGS AND SUPPORT
Please report bugs and ideas in GitHub Issues. The source is at https://github.com/koorchik/Validator-LIVR.
You can read this documentation with perldoc Validator::LIVR, or on MetaCPAN.
AUTHOR
Viktor Turskyi, <koorchik at cpan.org>
LICENSE AND COPYRIGHT
Copyright 2012-2026 Viktor Turskyi.
This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License (2.0). You may obtain a copy of the full license at:
https://www.perlfoundation.org/artistic-license-20.html
Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.
If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.
This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.
This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed.
Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.