Structure of a Modddel
A Modddel typically looks like this :
// 1. Imports and Part statements
import 'package:modddels_annotation_fpdart/modddels_annotation_fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'age.modddel.dart';
part 'age.freezed.dart';
// 2. The `@Modddel` Annotation
@Modddel(
validationSteps: [
ValidationStep([
Validation('legal', FailureType<AgeLegalFailure>()),
], name: 'Value'),
],
)
// 3. The class declaration
class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
// 4. The private empty constructor
Age._();
// 5. The factory constructor
factory Age(int value) {
return _$Age._create(
value: value,
);
}
// 6. The validate methods
@override
Option<AgeLegalFailure> validateLegal(age) {
if (age.value < 18) {
return some(const AgeLegalFailure.minor());
}
return none();
}
}
// 7. The Failure(s)
@freezed
class AgeLegalFailure extends ValueFailure with _$AgeLegalFailure{
const factory AgeLegalFailure.minor() = _Minor;
}
1. Imports and Part statements
For the Modddels generator to work, you need to import modddels_annotation_fpdart
or modddels_annotation_dartz
(depending on which package you installed), and add the part statement filename.modddel.dart
, where filename
is the name of the file.
If you're using freezed for making the failures sealed classes, also import it and add the part statement filename.freezed.dart
, where filename
is the name of the file.
2. The @Modddel
Annotation
@Modddel
AnnotationThe @Modddel
annotation is where you can define the validationSteps of your modddel.
Example :
@Modddel(
validationSteps: [
ValidationStep([
Validation('size', FailureType<UsernameSizeFailure>()),
Validation('characters', FailureType<UsernameCharactersFailure>()),
], name: 'Form'),
ValidationStep([
Validation('reserved', FailureType<UsernameReservedFailure>()),
], name: 'Availability'),
],
)
As you can see, inside the validationSteps
parameter, you provide a list of ValidationStep
s in the order that you want them to be processed.
For each ValidationStep
, you provide its name
, as well as the list of validations
it contains (the order doesn't matter here). The name
is mainly used to make the name of the invalid-step union-case class name (Ex : 'Form' → 'InvalidUsernameForm'). It must begin with an uppercase letter, and it must be made of valid dart identifier characters (alphanumeric, underscore and dollar sign).
For each Validation
, you provide its name
, as well as the failureType
. The name
is used to make the name of the validation method (Ex : 'size' → 'validateSize'), as well as the name of the failure (Ex : 'size' → 'sizeFailure'). It must begin with a lowercase letter, and it must be made of valid dart identifier characters. The failureType
is the type of the failure of the validation. You can either provide it as a typeArg : FailureType<UsernameSizeFailure>()
, or you can provide it as a string : FailureType('UsernameSizeFailure')
(the latter takes precedence).
3. The class declaration
When declaring your modddel class, you should extend the appropriate modddel kind, such as SingleValueObject
, MultiValueObject
, SimpleEntity
... , and provide two type arguments. The first type argument should be 'Invalid' followed by the modddel name, and the second type argument should be 'Valid' followed by the modddel name.
Your modddel class should also mix in a mixin with the name of your modddel prefixed by _$
(Like freezed).
class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
// ...
4. The private empty constructor
The modddel should only contain one generative constructor, which should be private and empty. Example : Age._()
. You should never use this constructor to create an instance of the Modddel.
5. The factory constructor
The parameters of the factory constructor will be the list of properties the modddel contains. Parameters can be positional or named, optional or required, and can have default values.
The factory constructor should always return the result of calling the mixin's _create
static method. This method is generated by the modddels generator, and you should pass to it all the parameters of the factory.
Example :
factory FullName(
String firstName, {
required String lastName,
}) {
// Notice how all the parameters of the `_create` method are named.
// This is so that you don't mix-up the order of the arguments.
return _$FullName._create(
firstName: firstName,
lastName: lastName,
);
}
Your factory constructor should be unnamed unless you want to create a union of modddels.
Decorators and comments
You can document/decorate a field by documenting/decorating its parameter inside the factory constructor.
Example : Documenting firstName
and annotating lastName
with @Deprecated
:
factory FullName(
/// The firstName of the user.
///
/// This will be visible to other users.
String firstName, {
@Deprecated('Will be removed') required String lastName,
})
Asserts & Sanitization
The factory constructor allows you to do all sorts of data manipulation or sanitization before effectively creating the modddel.
Example :
factory FullName(
String firstName, {
required String lastName,
}) {
assert(firstName.isNotEmpty && lastName.isNotEmpty);
final sanitizedFirstName = firstName.trim();
final sanitizedLastName = lastName.trim();
return _$FullName._create(
firstName: sanitizedFirstName,
lastName: sanitizedLastName,
);
}
6. The validate methods
After running the generator, your IDE will prompt you to override the "validate" method(s) :

This is the place where you can implement your validation logic. For example :
@override
Option<AgeLegalFailure> validateLegal(_ValidateAgeLegal age) {
if (age.value < 18) {
return some(const AgeLegalFailure.minor());
}
return none();
}
The age
parameter contains all the fields of the Modddel. You can omit its type for a cleaner look :
@override
Option<AgeLegalFailure> validateLegal(age) {
...
}
You should NEVER access an instance member (property, method, getter...) from within the "validate" methods. Doing so will throw a runtime error.
@override
Option<AgeLegalFailure> validateLegal(age) {
// Bad, will throw a runtime error.
final val = this.value;
// Good
final val = age.value;
}
7. The Failure(s)
In the example, the Failure is created using freezed.
@freezed
class AgeLegalFailure extends ValueFailure with _$AgeLegalFailure{
const factory AgeLegalFailure.minor() = _Minor;
}
Note that the failure should always extend ValueFailure
if the modddel is a ValueObject, and it should extend EntityFailure
if the modddel is an Entity.