Skip to content

Results and issues

Luthor never throws for invalid input. validate and validateSchema return a ValidationResult<T> with two cases: ValidationSuccess<T> holding the output, and ValidationFailure<T> holding a list of issues. Everything else you can read from a result is derived from those issues. The reference lists the members.

The result is sealed, so a switch has to handle both cases and the compiler tells you when one is missing. Object patterns pull the fields out.

final result = l.string().email().required().validate(input);
switch (result) {
case ValidationSuccess(:final data):
sendTo(data);
case ValidationFailure(:final issues):
for (final issue in issues) {
log(issue.code, issue.errorPath, issue.message);
}
}

T is the validator’s output type, String here. With validateSchema and a fromJson, T is your model.

A ValidationIssue records one problem: a code such as IssueCode.tooSmall, the path to the value inside the input, the message, the params the check used, and the fieldName that went into the message.

final issue = l.int().min(18).withName('age').validate(12).issues.single;
print(issue);
ValidationIssue(code: tooSmall, path: , message: age must be greater than or equal to 18)

Switch on the code when the UI needs to react to the kind of problem rather than show the text. The set of codes is open, so keep a default case. ValidationIssue and IssueCode lists every code and its params.

Every result, success or failure, exposes the issues in four shapes. On a success all four are empty, so you can read them without matching first.

View Type Use it for
issues List<ValidationIssue> Logging, telemetry, switching on codes
messages List<String> A flat list of text, for a toast or a log line
errors Map<String, List<String>> Form fields: messages grouped by error path
getError(path), getErrors(path) String?, List<String> One field’s first message, or all of them
final user = l.schema({
'email': l.string().email().required(),
'age': l.int().min(18),
'profile': l.schema({
'displayName': l.string().min(2).required(),
}).required(),
});
final result = user.validate({
'email': 'bad',
'age': 12,
'profile': {'displayName': 'x'},
});
result.errors
{
"email": [
"email must be a valid email address"
],
"age": [
"age must be greater than or equal to 18"
],
"profile.displayName": [
"displayName must be at least 2 characters long"
]
}
result.getError('email'), ('profile.displayName'), ('profile'), ('age')
email must be a valid email address
displayName must be at least 2 characters long
null
age must be greater than or equal to 18

getError('profile') is null because the issue sits at profile.displayName, not at profile. Errors and error paths explains the paths.

A schema or list returns a new collection of the validated values. For a schema, that means unknown keys are gone unless the schema has .passthrough():

user.validate({...valid, 'extra': 1})
ValidationSuccess(data: {email: [email protected], profile: {displayName: Ada}})

validateSchema takes a fromJson and runs it on the output map after every field passed. The success case then holds the model.

final result = user.validateSchema(json, fromJson: User.fromJson);
ValidationSuccess(data: User(email: [email protected], age: null))

Two things that used to throw are now failures: a fromJson that throws becomes a fromJsonFailed issue, and null input becomes a required issue.

ValidationFailure(errors: {(root): [value could not be converted]})
ValidationFailure(errors: {(root): [value is required]})

Both cases override toString. The failure form prints the error map, with object-level errors under (root) instead of the empty key. Only the printed form changes: errors and getError('') still use ''.

ValidationSuccess(data: [email protected])
ValidationFailure(errors: {(root): [value must be a valid email address]})
ValidationFailure(errors: {email: [email must be a valid email address]})