Tests CPAN version

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 LIVR 2.5, and passes the test suites of both. The rules added in LIVR 2.5 are in "LIVR 2.5 rules".

  • 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 REQUIRED or TOO_SHORT. Turn them into messages in any language you need.

  • Data cleanup with modifiers, such as trim, to_lc and default.

  • 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

Specification

required

The field is present and not empty: not undef and 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

Specification

string

Any string or number. Error: FORMAT_ERROR.

{ eq => $value }

The value is equal to $value, compared as strings: 2 equals "2", and a boolean such as JSON::PP::true equals "true". As in Validator::LIVR 2.0, true also equals "1" and false "0". The output is $value. Error: NOT_ALLOWED_VALUE.

{ one_of => [ @values ] }

The value is equal to one of @values, compared as in eq. As in Validator::LIVR 2.0, a boolean value also equals 1 or "1" (true) and 0 or "0" (false). The output is that one of @values. Error: NOT_ALLOWED_VALUE.

{ min_length => $length }

The value has at least $length characters. Error: TOO_SHORT.

{ max_length => $length }

The value has at most $length characters. Error: TOO_LONG.

{ length_equal => $length }

The value has exactly $length characters. Errors: TOO_SHORT, TOO_LONG.

{ length_between => [ $min, $max ] }

The value has from $min to $max characters. Errors: TOO_SHORT, TOO_LONG.

{ like => $regex }, { like => [ $regex, 'i' ] }

The value matches the regular expression. The i flag 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

Specification

integer

An integer, such as -5 or 42. Error: NOT_INTEGER.

positive_integer

An integer greater than zero. Error: NOT_POSITIVE_INTEGER.

decimal

A number, such as -1.5 or 42. 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 $min to $max. Errors: NOT_NUMBER, TOO_LOW, TOO_HIGH.

Special rules

Specification

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 http or https URL, such as https://example.com/path?query#fragment. The host is a domain name, such as example.com or intranet, or an IPv4 address. Domain names in other languages work in decoded strings, not in UTF-8 bytes. Error: WRONG_URL.

iso_date, { iso_date => { min => $date, max => $date } }

A date as YYYY-MM-DD that exists in the calendar: 2024-02-29 is valid, 2025-02-29 is not. Error: WRONG_DATE.

LIVR 2.5: the bounds min and max, both optional and inclusive, are dates or today, yesterday and tomorrow: the current date in UTC, from the system time (see "Testing rules with the current date"). An argument that is not a hash dies; LIVR 2.0 ignored it. A boolean gives WRONG_DATE. Errors: WRONG_DATE, DATE_TOO_EARLY, DATE_TOO_LATE, FORMAT_ERROR.

{ equal_to_field => $field }

The value is equal to the value of another field, such as a password confirmation. Error: FIELDS_NOT_EQUAL.

Metarules

Specification

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 $selector field. The error is FORMAT_ERROR when 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' ] },

The rules of or and of aliases see the fields next to the field, so equal_to_field works in them. The items of list_of have no fields next to them.

Modifiers

Specification

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-67 into 0501234567.

{ leave_only => $chars }

Removes all characters except the ones in $chars.

{ default => $value }

Sets $value when the field is missing or empty.

LIVR 2.5 rules

LIVR 2.5 specification, all rules on the site

The rules added in LIVR 2.5. They are loaded on first use, so they do not slow down use Validator::LIVR. They check their arguments when the validator is built: "prepare" dies on a wrong argument, such as { boolean => 1 } or { digest => 'MD5' }, or on undef or an empty list in place of rules in when, values and keys (use any for no rules), and on undef or a list in their lists of rules, such as [ 'required', [] ]. Arguments after the ones a rule takes are ignored, and a number and a string with that number are the same argument. undef is an argument too, and no LIVR 2.5 rule takes it: { boolean => undef } dies, like { boolean => [undef] }. The LIVR 2.0 rules and custom rules get undef as an argument, as before, and { iso_date => undef } stays iso_date, as LIVR 2.0 ignored its arguments.

They compare and check the string form of a value: a boolean, such as JSON::PP::true, is true or false. Format and checksum rules return their own error code for a boolean, and FORMAT_ERROR for a hash or a list.

boolean

true, false, 1, 0, "1", "0", "true" or "false". The output is JSON::PP::true or JSON::PP::false. Errors: NOT_BOOLEAN, FORMAT_ERROR.

{ not_one_of => [ @values ] }

The value is none of @values. Errors: NOT_ALLOWED_VALUE, FORMAT_ERROR.

{ max_decimals => $count }

A number with at most $count digits after the decimal point; trailing zeros do not count. The output is a number. Errors: TOO_MANY_DECIMALS, NOT_NUMBER, FORMAT_ERROR.

digits

ASCII digits, the leading zeros are kept: the output is a string. Errors: NOT_DIGITS, FORMAT_ERROR.

{ starts_with => [ @strings ] }, { ends_with => [ @strings ] }, { contains => [ @strings ] }

The value starts with, ends with or contains one of @strings. Errors: WRONG_FORMAT, FORMAT_ERROR.

iso_datetime, { iso_datetime => { min => $bound, max => $bound } }

A date and a time with an offset, such as 2026-10-29T10:00+02:00. The output is the time in UTC, 2026-10-29T08:00:00.000Z. A bound is a datetime, a date (its start as min, its end as max) or today, yesterday and tomorrow. Errors: WRONG_DATETIME, DATETIME_TOO_EARLY, DATETIME_TOO_LATE, FORMAT_ERROR.

iso_time, { iso_time => { min => $time, max => $time } }

HH:MM, HH:MM:SS or HH:MM:SS.fraction. With min greater than max the range goes over midnight. Errors: WRONG_TIME, TIME_TOO_EARLY, TIME_TOO_LATE, FORMAT_ERROR.

{ required_if => { $reference => $value, ... } }

The field is required when all referenced fields have the given values, or one of the values of a list. A reference is a field name or a path, such as address/country. Error: REQUIRED.

{ when => [ $reference, $condition, $rules_if_true, $rules_if_false ] }, { when => [ $reference, { $value => $rules, ... } ] }

Validates the field with rules that depend on another field: on whether it passes the $condition rules, or on its value.

vat_number => { when => [ 'type', { eq => 'company' }, [ 'required', 'string' ], { eq => '' } ] },

Errors: NOT_ALLOWED_VALUE and the errors of the rules.

{ values => \@rules }, { keys => \@rules }

A hash with every value or every key validated with the rules. A wrong key gets WRONG_KEY. Errors: WRONG_KEY, FORMAT_ERROR and the errors of the rules.

any

Any value, the hashes and lists too, stays in the output unchanged.

{ list_length => $length }, { list_length => [ $min, $max ] }, { list_length => { min => $min, max => $max } }

A list with that number of items. Errors: TOO_FEW_ITEMS, TOO_MANY_ITEMS, FORMAT_ERROR.

unique, { unique => [ @keys ] }

A list without duplicates, or of hashes without duplicate values of @keys. Errors: NOT_UNIQUE_ITEMS, FORMAT_ERROR.

{ split => $separator }

A modifier: splits a string into a list, "1,2,3" into ["1", "2", "3"]. A number or a boolean is split in its string form, a list stays as it is.

Formats

uuid (or { uuid => [ 'v4', 'v7' ] }), ip (or { ip => 'v4' }, { ip => 'v6' }), cidr (the same), hostname, base64 (or { base64 => 'url' }), hex, { digest => [ 'md5', 'sha256' ] } (also sha1, sha384, sha512), mongo_id, hex_color, mac (or { mac => 64 }), e164, semver and jwt. Errors: WRONG_UUID, WRONG_IP, WRONG_CIDR, WRONG_HOSTNAME, WRONG_BASE64, WRONG_HEX, WRONG_DIGEST, WRONG_MONGO_ID, WRONG_HEX_COLOR, WRONG_MAC, WRONG_E164, WRONG_SEMVER, WRONG_JWT, FORMAT_ERROR.

Checksums

luhn, credit_card and iban. The value is checked as it is: remove spaces before, with { remove => ' -' }. Errors: WRONG_CHECKSUM, WRONG_CREDIT_CARD_NUMBER, WRONG_IBAN, FORMAT_ERROR.

Testing rules with the current date

The bounds today, yesterday and tomorrow read the system time with time. To fix it in tests, load Test::MockTime before the first validator with such a bound is built:

use Test::MockTime qw(set_fixed_time);
use Validator::LIVR;

set_fixed_time('2026-10-18T12:00:00Z', '%Y-%m-%dT%H:%M:%SZ');

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, which the builder must not change:

'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_field does. Inside or and aliases, it is the hash of the field that uses them. Inside list_of, values and keys, it is empty.

$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. The LIVR 2.5 rules are in Validator::LIVR::Rules::Types, Validator::LIVR::Rules::DateTime, Validator::LIVR::Rules::Conditional, Validator::LIVR::Rules::Collection and Validator::LIVR::Rules::Format.

SEE ALSO

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.