Imported from Flutterando/lucid_validation (
SKILL.md). Install upstream withnpx skills add Flutterando/lucid_validation. Copyright stays with the author.
LucidValidation AI Developer Guide
This guide contains everything an AI assistant or human developer needs to know to work with the LucidValidation package. Use it as a reference for syntax, API surface, architectural patterns, and extension methodologies.
1. Core Architecture
LucidValidation is inspired by FluentValidation (C#). It evaluates validation rules against models s s.
Key Classes
LucidValidator<E>: The base abstract class. Subclasses extend this with their target entity typeE(e.g.UserValidator extends LucidValidator<User>).- Registers validation builders in the constructor.
- Triggers validations via
validateorvalidateAsync.
LucidValidationBuilder<TProp, Entity>: Extends the validator to define rules for a specific property of typeTPropextracted from the parentEntity.SimpleValidationBuilder<T>: Alias ofLucidValidationBuilder<T, T>, used when validating primitive values or when writing extensions that don't depend on the parent entity.ValidationException: Holds validation details (message, code, key, entity, index).ValidationResult: Wraps the execution result (isValid,exceptions,errorsByKey,firstErrorFor).
Sync vs. Async Rule Engine
[!IMPORTANT]
- Sync validations: Registered using
must,use, oruseValidation. Executed viavalidate(entity).- Async validations: Registered using
mustAsync,useAsync, oruseValidationAsync. Executed viavalidateAsync(entity).- Critical Pitfall: If a validator contains any async rules, calling
validate()will throw anAsyncValidationException. You must usevalidateAsync()in this case.
2. Built-In Validators
All built-in validation rules are implemented as extensions on SimpleValidationBuilder<T> or LucidValidationBuilder<T, E>.
Common & Core
must(bool Function(T value) predicate, String message, String code): Custom check.mustWith(bool Function(T value, E entity) predicate, String message, String code): Custom check with entity context.mustAsync(Future<bool> Function(T value) predicate, ...): Async custom check.mustWithAsync(Future<bool> Function(T value, E entity) predicate, ...): Async custom check with entity context.isNull(),isNotNull(): Check for nullability.equalTo(T Function(E) comparison): Check equality to another field.notEqualTo(T Function(E) comparison): Check inequality to another field.isInEnum(List<T> values): Check value is in allowed list (e.g.MyEnum.values).
String Checks
isEmpty(),notEmpty(): Checks if string is empty / not empty.minLength(int),maxLength(int),length(min, max): Length constraints.isAlphanumeric(),isNumeric(): Character set validations.isUppercase(),isLowercase(): Casing constraints.contains(String),startsWith(String),endsWith(String): Substring checks.matchesPattern(Pattern): Regular expression validation.mustHaveLowercase(),mustHaveUppercase(): Character type presence.mustHaveNumber(),mustHaveSpecialCharacter(): Character type presence (note:mustHaveNumberis singular).hasNoSequentialRepeatedCharacters(int length): Rejects sequences like"aaa".hasNoSequentialCharacters(int length): Rejects sequences like"123"or"abc".
Numeric & Boolean Checks
min(num),max(num): Value bounds.range(min, max),inclusiveBetween(min, max),exclusiveBetween(min, max): Range bounds.isPositive(),isNegative(),isNonNegative(),isNonZero(): Sign checks.isEven(),isOdd(): Modulo checks.multipleOf(num factor): Divisibility check.precisionScale(int precision, int scale): Digits and decimal bounds.isTrue(),isFalse(): Strictly boolean checks.
DateTime Checks
greaterThan(DateTime),greaterThanOrEqualTo(DateTime): Min date checks.lessThan(DateTime),lessThanOrEqualTo(DateTime): Max date checks.inclusiveBetween(DateTime, DateTime),exclusiveBetween(DateTime, DateTime): Date range checks.inPast(),inFuture(): Time-relative checks.afterField(DateTime Function(E) other): Asserts date is after another property.beforeField(DateTime Function(E) other): Asserts date is before another property.
Collections & Lists
minItems(int),maxItems(int): Iterable length bounds.listContains(T item): Item presence in list.
Web, Network, & Identification
validEmail(): Valid email format.validUrl(),httpUrl(): Valid URL / HTTP(S) URL.validUuid(): Valid UUID format (note:validUuidis camelCased).validIpv4(),validIpv6(): IPv4 / IPv6 addresses (note: lowercasepv).validCreditCard(): Luhn algorithm check.
Brazilian Formats
validCPF(),validCNPJ(),validCPFOrCNPJ(),validCEP(): Brazilian national identifiers.validPhoneBR(),validPhoneWithCountryCodeBR(): Brazilian telephone numbers.
3. Composition & Complex Nesting
Validator Composition (include)
To combine multiple validators for the same entity class:
class FullUserValidator extends LucidValidator<UserModel> {
FullUserValidator() {
include(ContactValidator());
include(BillingValidator());
}
}
Nesting Single Objects (setValidator)
Use setValidator to delegate validation of a sub-property to a dedicated validator:
class CustomerValidator extends LucidValidator<Customer> {
CustomerValidator() {
ruleFor((c) => c.address, key: 'address').setValidator(AddressValidator());
}
}
Nesting Iterables (setEach)
Use setEach to apply a validator to every item inside a collection, which automatically tracks item index:
ruleFor((c) => c.students, key: 'students').setEach(StudentValidator());
Inline Iterable Validation (ruleForEach)
Use ruleForEach when validating lists of primitive values or inline rules:
ruleForEach((order) => order.tags, key: 'tags').notEmpty().maxLength(10);
4. Localization & Custom Languages
Global language setting is managed via the Culture object:
LucidValidation.global.culture = Culture('en', 'US');
Overriding Translations Globally
Override individual translations s s dynamically using LanguageManager:
class CustomLanguageManager extends LanguageManager {
CustomLanguageManager() {
// Override a specific translation code
addTranslation(Culture('pt', 'BR'), Language.code.equalTo, 'O valor deve ser idêntico a {ComparisonValue}.');
}
}
// Assign it globally
LucidValidation.global.languageManager = CustomLanguageManager();
Implementing a New Language
To add a new language, subclass Language from lib/src/localization/language.dart and pass a map of s s translation keys in the call to super():
import 'package:lucid_validation/src/localization/language.dart';
class SpanishLanguage extends Language {
SpanishLanguage()
: super({
Language.code.notEmpty: "'{PropertyName}' no puede estar vacÃo.",
Language.code.validEmail: "'{PropertyName}' no es un correo electrónico válido.",
// Add translations for all Language.code.* keys
});
}
5. Creating Custom Validation Rules
Custom rules are declared as Dart extensions on SimpleValidationBuilder<T> (for entity-agnostic rules) or LucidValidationBuilder<T, E> (for entity-aware rules).
Method 1: Composing Existing Rules
extension StrongPasswordValidation on SimpleValidationBuilder<String> {
SimpleValidationBuilder<String> strongPassword() {
return notEmpty()
.minLength(8)
.mustHaveLowercase()
.mustHaveUppercase()
.mustHaveNumber()
.mustHaveSpecialCharacter();
}
}
Method 2: Synchronous Rules using useValidation (Simple Predicate)
Use this when you want the validation engine to handle formatting and ValidationException creation:
extension CustomMinWords on SimpleValidationBuilder<String> {
SimpleValidationBuilder<String> minWords(int min, {String? message, String? code}) {
return useValidation(
(value, entity) => value.trim().split(RegExp(r'\s+')).length >= min,
code: code ?? 'minWords',
message: message ?? "'{PropertyName}' must have at least {Min} words.",
parameters: (value, entity) => {'Min': '$min'},
);
}
}
Method 3: Asynchronous Rules using useValidationAsync
For database lookups or HTTP calls, s:
extension UniqueUsername on SimpleValidationBuilder<String> {
SimpleValidationBuilder<String> uniqueUsername(
Future<bool> Function(String username) isUnique, {
String? message,
String? code,
}) {
return useValidationAsync(
(value, entity) => isUnique(value),
code: code ?? 'usernameTaken',
message: message ?? "'{PropertyName}' is already taken.",
);
}
}
Method 4: S/Async Rules with Full Exception Control (use / useAsync)
Use use (sync) or useAsync (async) when you want to construct the ValidationException manually (e.g. dynamic keys, nested properties, customized entity names):
extension CustomPrefixCheck on SimpleValidationBuilder<String> {
SimpleValidationBuilder<String> checkPrefix(String prefix) {
return use((value, entity) {
if (!value.startsWith(prefix)) {
return ValidationException(
key: key,
message: "'{PropertyName}' must start with '$prefix'.",
code: 'invalid_prefix',
entity: 'CustomEntity',
);
}
return null; // Return null if validation passes
});
}
}
6. Advanced Practical Examples
Example A: Flutter Form Integration
You can integrate validations in your Flutter forms by defining a dedicated validator class (recommended for clean architecture / Domain Layer validations) or inline directly within your Widget (recommended for rapid prototyping or purely UI-focused validation).
Option A1: Class-Based Validator (Recommended for Domain Layer)
First, create a dedicated validation class:
import 'package:lucid_validation/lucid_validation.dart';
class UserRegisterForm {
String email = '';
String username = '';
}
class UserRegisterValidator extends LucidValidator<UserRegisterForm> {
UserRegisterValidator() {
ruleFor((c) => c.email, key: 'email')
.notEmpty()
.validEmail();
ruleFor((c) => c.username, key: 'username')
.notEmpty()
.minLength(3);
}
}
Then, use it in your Widget:
import 'package:flutter/material.dart';
import 'package:lucid_validation/lucid_validation.dart';
class RegisterFormWidget extends StatelessWidget {
final credentials = UserRegisterForm();
final validator = UserRegisterValidator();
RegisterFormWidget({super.key});
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(hintText: 'Email'),
onChanged: (val) => credentials.email = val,
validator: validator.byField(credentials, 'email'),
),
TextFormField(
decoration: const InputDecoration(hintText: 'Username'),
onChanged: (val) => credentials.username = val,
validator: validator.byField(credentials, 'username'),
),
],
),
);
}
}
Option A2: Inline Validator (LucidValidator.inline)
Define the validator directly within your Widget to keep validations local to the UI file:
import 'package:flutter/material.dart';
import 'package:lucid_validation/lucid_validation.dart';
class RegisterFormWidgetInline extends StatelessWidget {
final credentials = UserRegisterForm();
// Define the validator inline on the class
final validator = LucidValidator<UserRegisterForm>.inline((v) {
v.ruleFor((c) => c.email, key: 'email')
.notEmpty()
.validEmail();
v.ruleFor((c) => c.username, key: 'username')
.notEmpty()
.minLength(3);
});
RegisterFormWidgetInline({super.key});
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(hintText: 'Email'),
onChanged: (val) => credentials.email = val,
validator: validator.byField(credentials, 'email'),
),
TextFormField(
decoration: const InputDecoration(hintText: 'Username'),
onChanged: (val) => credentials.username = val,
validator: validator.byField(credentials, 'username'),
),
],
),
);
}
}
Example B: Real-Time Password Strength Checklist
This example demonstrates how to use rulesForField to drive a checklist UI indicating to the user in real-time which password criteria are met as they type.
import 'package:flutter/material.dart';
import 'package:lucid_validation/lucid_validation.dart';
class PasswordChecklistWidget extends StatefulWidget {
const PasswordChecklistWidget({super.key});
@override
State<PasswordChecklistWidget> createState() => _PasswordChecklistWidgetState();
}
class _PasswordChecklistWidgetState extends State<PasswordChecklistWidget> {
String _password = '';
final validator = LucidValidator<String>.inline((v) {
v.ruleFor((p) => p, key: 'password')
.notEmpty().withErrorCode('required')
.minLength(8).withErrorCode('length')
.mustHaveUppercase().withErrorCode('uppercase')
.mustHaveNumber().withErrorCode('number')
.mustHaveSpecialCharacter().withErrorCode('special');
});
@override
Widget build(BuildContext context) {
// Evaluate all rules for the password field against the current string state
final rules = validator.rulesForField(_password, 'password');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
onChanged: (value) => setState(() => _password = value),
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
...rules.map((rule) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Icon(
rule.isValid ? Icons.check_circle : Icons.cancel,
color: rule.isValid ? Colors.green : Colors.red,
size: 20,
),
const SizedBox(width: 8),
Text(
rule.message,
style: TextStyle(
color: rule.isValid ? Colors.green[800] : Colors.red[800],
),
),
],
),
);
}),
],
);
}
}
Example C: Complex Nested Entity Graph Validation
This example showcases a full order-placement schema with:
- Nested object validation (
setValidator). - List validation with item index tracking (
setEachandminItems). - Polymorphic branching of nested objects based on a discriminator (
switchOn).
import 'package:lucid_validation/lucid_validation.dart';
// Domain Models
class Order {
final Customer customer;
final List<OrderItem> items;
final PaymentDetails payment;
final String status;
Order({
required this.customer,
required this.items,
required this.payment,
required this.status,
});
}
class Customer {
final String name;
final String email;
Customer({required this.name, required this.email});
}
class OrderItem {
final String sku;
final int quantity;
final double price;
OrderItem({required this.sku, required this.quantity, required this.price});
}
class PaymentDetails {
final String type; // 'credit', 'pix', 'bank_transfer'
final String? cardNumber;
final String? pixKey;
PaymentDetails({required this.type, this.cardNumber, this.pixKey});
}
// Validator
class OrderValidator extends LucidValidator<Order> {
OrderValidator() {
// Flat field check
ruleFor((o) => o.status, key: 'status').notEmpty();
// Nested object validator
ruleFor((o) => o.customer, key: 'customer').setValidator(
LucidValidator<Customer>.inline((v) {
v.ruleFor((c) => c.name, key: 'name').notEmpty();
v.ruleFor((c) => c.email, key: 'email').notEmpty().validEmail();
}),
);
// List validation with item index tracking
ruleFor((o) => o.items, key: 'items')
.minItems(1, message: 'Your order must contain at least one item.')
.setEach(
LucidValidator<OrderItem>.inline((v) {
v.ruleFor((i) => i.sku, key: 'sku').notEmpty().maxLength(10);
v.ruleFor((i) => i.quantity, key: 'quantity').min(1);
v.ruleFor((i) => i.price, key: 'price').isPositive();
}),
);
// Nested object with polymorphic validation using switchOn
ruleFor((o) => o.payment, key: 'payment').setValidator(
LucidValidator<PaymentDetails>.inline((v) {
v.ruleFor((p) => p.type, key: 'type').notEmpty();
v.switchOn((p) => p.type, cases: {
'credit': (inner) {
inner.ruleFor((p) => p.cardNumber!, key: 'cardNumber').notEmpty().validCreditCard();
},
'pix': (inner) {
inner.ruleFor((p) => p.pixKey!, key: 'pixKey').notEmpty();
},
});
}),
);
}
}
// Executing Validation
void main() {
final order = Order(
status: 'pending',
customer: Customer(name: 'Alice', email: 'invalid-email'),
items: [
OrderItem(sku: 'SKU123', quantity: 0, price: -5.0), // invalid quantity/price
],
payment: PaymentDetails(type: 'credit', cardNumber: ''), // missing credit card number
);
final result = OrderValidator().validate(order);
print(result.isValid); // false
print(result.errorsByKey);
// Output maps errors to nested paths & indices:
// {
// "customer.email": ["'email' is not a valid email address."],
// "items.quantity": ["'quantity' must be greater than or equal to 1. You entered 0."],
// "items.price": ["'price' must be a positive number."],
// "payment.cardNumber": ["'cardNumber' must not be empty."]
// }
}