Skip to content

Your first validator

In this tutorial you build a validator for an email address, run it against three inputs, and branch on the result. You need a Dart project with luthor installed. Installation covers that.

  1. Create bin/main.dart and start from l.

    Every validator starts from l, the validator factory. Calling a type method such as string() returns a validator that checks the Dart type. Each modifier you chain after it returns a new validator with one more validation.

    bin/main.dart
    import 'package:luthor/luthor.dart';
    void main() {
    final email = l.string().email().required();
    }

    l.string() checks that the value is a String. .email() checks that it is an email address. .required() rejects null, and it also changes the type: email is a StringValidator<String>, so a successful result holds a String, not a String?.

  2. Validate a value.

    Call validate. It accepts any input, returns a ValidationResult<String>, and never throws. isValid tells you which case you got.

    bin/main.dart
    import 'package:luthor/luthor.dart';
    void main() {
    final email = l.string().email().required();
    print(email.validate('[email protected]').isValid);
    print(email.validate('bad').isValid);
    print(email.validate(null).isValid);
    }

    Run it with dart run bin/main.dart. You should see:

    dart run bin/main.dart
    true
    false
    false
  3. Read the result.

    ValidationResult is sealed with two cases, ValidationSuccess and ValidationFailure. Replace the three prints with a switch. The compiler requires both cases, and the success case gives you the typed value.

    bin/main.dart
    import 'package:luthor/luthor.dart';
    void main() {
    final email = l.string().email().required();
    for (final input in ['[email protected]', 'bad', null]) {
    switch (email.validate(input)) {
    case ValidationSuccess(:final data):
    print('+ $data');
    case ValidationFailure(:final messages):
    print('- $input: ${messages.first}');
    }
    }
    }
    dart run bin/main.dart
    - bad: value must be a valid email address
    - null: value is required

    The same run as a report:

    email.validate(input)+1-2
    1. valid: '[email protected]'
    2. invalid: 'bad'value must be a valid email address
    3. invalid: nullvalue is required
  4. Name the value.

    Messages say value because the validator has no name. Give it one with withName, and the name replaces value in every message the chain produces.

    bin/main.dart
    import 'package:luthor/luthor.dart';
    void main() {
    final email = l.string().email().required();
    final email = l.string().email().required().withName('email');
    email.validate(input)+0-2
    1. invalid: 'bad'email must be a valid email address
    2. invalid: nullemail is required
  5. Reuse the base validator.

    Validators are immutable. Chaining returns a new validator and leaves the one you chained from unchanged, so a base validator can branch into several without one leaking into another.

    final text = l.string();
    final email = text.email();
    final uuid = text.uuid();
    print(text.validate('anything').isValid);
    print(email.validate('anything').isValid);
    print(uuid.validate('anything').isValid);
    true
    false
    false

You have built a validator, run it, branched on the typed result, and named it. Next, Your first schema does the same for a map with several fields.