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 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 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. 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 $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

A date as YYYY-MM-DD that exists in the calendar: 2024-02-29 is valid, 2025-02-29 is 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

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' ] },

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.

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_field does. Inside list_of, or and 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

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.