Skip to content

Generate a schema from a model

In this tutorial you turn the User model from Your first schema into an annotated class, generate its schema, and validate input with the generated function. It uses freezed and json_serializable. Models without freezed shows the other setups. The generated code on this page is the real output of luthor_generator for the model shown.

  1. Add the packages.

    Terminal window
    dart pub add luthor freezed_annotation json_annotation
    dart pub add dev:build_runner dev:luthor_generator dev:freezed dev:json_serializable

    On Dart 3.11 use freezed 3.2.x. freezed 4 needs Dart 3.13 or later.

  2. Write the model.

    Annotate the class with @luthor next to @freezed. Put Luthor’s annotations on the constructor parameters: @IsEmail() adds .email(), @HasMin(18) on an int adds .min(18). Non-nullable parameters without a default become required fields; nullable ones stay optional.

    lib/user.dart
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:luthor/luthor.dart';
    part 'user.freezed.dart';
    part 'user.g.dart';
    @luthor
    @freezed
    abstract class User with _$User {
    const factory User({
    @HasMin(2) required String name,
    @IsEmail() required String email,
    @HasMin(18) int? age,
    }) = _User;
    factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    }
  3. Run the generator.

    Terminal window
    dart run build_runner build

    user.g.dart now holds five generated items for User: the schema $UserSchema, the validate function $UserValidate, the validateSelf extension, the schema keys UserSchemaKeys, and the error keys UserErrorKeys. The schema is the one you would have written by hand:

    lib/user.g.dart (excerpt)
    final SchemaValidator $UserSchema = l
    .schema({
    UserSchemaKeys.name: l.string().min(2).required(),
    UserSchemaKeys.email: l.string().email().required(),
    UserSchemaKeys.age: l.int().min(18),
    })
    .withName("User");
    ValidationResult<User> $UserValidate(Object? json) =>
    $UserSchema.validateSchema(json, fromJson: User.fromJson);
  4. Validate with the generated function.

    $UserValidate accepts any input, validates it, and on success converts it with User.fromJson. You get a ValidationResult<User>.

    bin/main.dart
    import 'package:luthor/luthor.dart';
    import 'package:my_app/user.dart';
    void main() {
    final result = $UserValidate({
    'name': 'Ada',
    'email': '[email protected]',
    'age': 36,
    });
    switch (result) {
    case ValidationSuccess(:final data):
    print('+ ${data.name} <${data.email}>');
    case ValidationFailure(:final errors):
    print('- $errors');
    }
    }
    dart run bin/main.dart, valid then invalid input
    - {name: [name must be at least 2 characters long], email: [email must be a valid email address], age: [age must be greater than or equal to 18]}
    $UserValidate(input)+2-2
    1. valid: {'name': 'Ada', 'email': '[email protected]', 'age': 36}
    2. valid: {'name': 'Ada', 'email': '[email protected]'}
    3. invalid: {'name': 'A', 'email': 'ada', 'age': 12}name: name must be at least 2 characters longemail: email must be a valid email addressage: age must be greater than or equal to 18
    4. invalid: 'not a map'User must be a map
  5. Read errors with the error keys.

    UserErrorKeys is a record whose fields hold the error path of each model field. Use it with getError instead of a string, and a renamed field becomes a compile error instead of a null.

    print(result.getError(UserErrorKeys.email));
    print(result.getError(UserErrorKeys.age));
    email must be a valid email address
    age must be greater than or equal to 18
  6. Validate an existing instance.

    The generated extension validates a model you already have, by serialising it with toJson() and running the schema.

    final user = User(name: 'A', email: 'ada', age: 12);
    print(user.validateSelf().isValid);
    print(user.validateSelf().errors);
    false
    {name: [name must be at least 2 characters long], email: [email must be a valid email address], age: [age must be greater than or equal to 18]}

That is the whole loop: annotate, generate, validate. What gets generated describes each generated item, and Supported field types lists how every Dart type maps to a validator.