JustValidation

English | Español

Pub Version Test Coverage Tests Dart License


🇬🇧 English

A fluent validation library for Dart inspired by FluentValidation (.NET). Build expressive, strongly-typed validation rules for your Dart and Flutter applications with parallel processing support via isolates.

✨ Features

  • Fluent API - Chainable, readable validation rules
  • Type-safe - Full support for Dart's type system and null-safety
  • Rich Validators - 30+ built-in validators for common scenarios
  • Advanced Validators - IP, MAC, GPS, Credit Card, IBAN, ISBN, UUID, Postal Codes (129 countries)
  • DateTime Validators - Past/Future, Age validation, Date ranges, Same day checks
  • Identity Documents - 26 validators for 18 countries (DNI, SSN, CPF, CURP, etc.)
  • Parallel Processing - Isolate-based validation for large batches (1000+ objects)
  • Custom Validators - Easy to add custom validation logic
  • Async Validation - Built-in support for asynchronous validation
  • Conditional Rules - When/Unless conditions for dynamic validation
  • Nested Validation - Validate complex object graphs
  • Collection Validation - Validate collections and their items
  • Inline Validation - Quick validation without creating validator classes
  • Zero Dependencies - Lightweight with no external dependencies
  • Universal - Works with Dart VM, Flutter, Web, and backend

📦 Installation

Add just_validation to your pubspec.yaml:

dependencies:
  just_validation: ^0.3.0

environment:
  sdk: '>=2.19.0 <4.0.0'

Then run:

dart pub get
# or
flutter pub get

🚀 Quick Start

import 'package:just_validation/just_validation.dart';

// Define your model
class User {
  final String? name;
  final String? email;
  final int? age;

  User({this.name, this.email, this.age});
}

// Create a validator
class UserValidator extends AbstractValidator<User> {
  UserValidator() {
    ruleFor((user) => user.name)
        .notEmpty()
        .withMessage('Name is required')
        .minLength(2)
        .maxLength(50);
    
    ruleFor((user) => user.email)
        .notEmpty()
        .emailAddress()
        .withMessage('Invalid email address');
    
    ruleFor((user) => user.age)
        .greaterThanOrEqual(18)
        .withMessage('Must be 18 or older')
        .lessThan(120)
        .when((user) => user.age != null);
  }
}

// Use the validator
void main() {
  final validator = UserValidator();
  final user = User(name: 'John', email: 'john@example.com', age: 25);
  
  final result = validator.validate(user);
  
  if (result.isValid) {
    print('✅ User is valid!');
  } else {
    for (var error in result.errors) {
      print('❌ ${error.propertyName}: ${error.errorMessage}');
    }
  }
}

⚡ Parallel Processing with Isolates

For large batches or computationally expensive validations, use isolate-based validation:

// Validate 10,000 objects without blocking the main thread
final users = List.generate(10000, (i) => User(...));

// Instead of:
// final results = users.map((u) => validator.validate(u)).toList(); // Blocks UI

// Use isolates:
final results = await validator.validateManyIsolate(users); // Non-blocking!

print('✅ Validated ${results.length} users in parallel');

When to use isolates:

  • ✅ Batch processing (1000+ objects)
  • ✅ Complex validations (50+ rules)
  • ✅ Cannot block UI thread (Flutter apps)
  • ✅ Background processing

When NOT to use isolates:

  • ❌ Simple forms (1-100 fields)
  • ❌ Fast validations (<50ms total)
  • ❌ Small batches (<100 objects)

📚 Documentation

String Validators

ruleFor((x) => x.name)
  .notEmpty()           // Must not be empty
  .notNull()            // Must not be null
  .minLength(2)         // Minimum length
  .maxLength(50)        // Maximum length
  .length(5, 20)        // Length between 5 and 20
  .matches(RegExp(r'^[a-zA-Z]+$'))  // Regex pattern
  .emailAddress()       // Valid email
  .url()                // Valid URL

Number Validators

ruleFor((x) => x.age)
  .greaterThan(0)            // > 0
  .greaterThanOrEqual(18)    // >= 18
  .lessThan(100)             // < 100
  .lessThanOrEqual(99)       // <= 99
  .inclusiveBetween(18, 65)  // Between 18 and 65 (inclusive)
  .exclusiveBetween(0, 100)  // Between 0 and 100 (exclusive)

Comparison Validators

ruleFor((x) => x.password)
  .equal('confirmed_password')       // Must equal
  .notEqual('old_password')          // Must not equal

Advanced Validators

// IP Address (IPv4, IPv6, or both)
ruleFor((x) => x.ip)
  .ipAddress()           // IPv4 or IPv6
  .ipv4Address()         // IPv4 only
  .ipv6Address()         // IPv6 only

// MAC Address
ruleFor((x) => x.mac)
  .macAddress()          // Supports :, -, or no separator

// GPS Coordinates
ruleFor((x) => x.lat)
  .latitude()            // -90 to 90
ruleFor((x) => x.lng)
  .longitude()           // -180 to 180

// Credit Card (Luhn algorithm)
ruleFor((x) => x.card)
  .creditCard()          // Visa, MC, Amex, etc.

// IBAN (52 countries)
ruleFor((x) => x.iban)
  .iban()                // Full Mod-97 validation

// ISBN (10 or 13)
ruleFor((x) => x.isbn)
  .isbn()                // ISBN-10 or ISBN-13

// UUID (v1-v5)
ruleFor((x) => x.uuid)
  .uuid()                // All UUID versions

// Postal Code (129 countries)
ruleFor((x) => x.zip)
  .postalCode('US')      // US ZIP codes
  .postalCode('ES')      // Spanish postal codes
  .postalCode('GB')      // UK postcodes
  // Supports 129 countries across all continents

DateTime Validators

ruleFor((x) => x.birthDate)
  .isInPast()                    // Must be in the past
  .minAge(18)                    // Must be 18+ years old
  .withMessage('Must be 18 or older');

ruleFor((x) => x.appointmentDate)
  .isInFuture()                  // Must be in the future
  .isBefore(DateTime(2025, 12, 31))  // Before end of year
  .isAfter(DateTime.now());      // After today

ruleFor((x) => x.eventDate)
  .isBetween(startDate, endDate) // Within date range (inclusive)
  .isToday()                     // Must be today
  .isSameDay(referenceDate);     // Same day as reference

ruleFor((x) => x.expiryDate)
  .isAfterOrEqual(DateTime.now())
  .isWithinDuration(DateTime.now(), Duration(days: 365));

Identity Document Validators

// European Documents
ruleFor((x) => x.document)
  .dni()                   // 🇪🇸 Spanish DNI
  .nie()                   // 🇪🇸 Spanish NIE
  .nin()                   // 🇬🇧 UK National Insurance
  .germanId()              // 🇩🇪 German Personalausweis
  .codiceFiscale()         // 🇮🇹 Italian Fiscal Code
  .nif()                   // 🇵🇹 Portuguese NIF
  .bsn()                   // 🇳🇱 Dutch BSN
  .belgianNationalNumber() // 🇧🇪 Belgian National Number
  .pesel()                 // 🇵🇱 Polish PESEL
  .frenchNationalId();     // 🇫🇷 French CNI

// American Documents
ruleFor((x) => x.document)
  .ssn()                   // 🇺🇸 US Social Security Number
  .curp()                  // 🇲🇽 Mexican CURP
  .rfc()                   // 🇲🇽 Mexican RFC
  .cpf()                   // 🇧🇷 Brazilian CPF
  .cnpj()                  // 🇧🇷 Brazilian CNPJ
  .cuit()                  // 🇦🇷 Argentine CUIT/CUIL
  .rut()                   // 🇨🇱 Chilean RUT
  .sin()                   // 🇨🇦 Canadian SIN
  .colombianCc()           // 🇨🇴 Colombian CC
  .ecuadorianCedula();     // 🇪🇨 Ecuadorian Cédula

// Asian Documents
ruleFor((x) => x.document)
  .chineseId()             // 🇨🇳 Chinese ID (18 digits)
  .aadhaar()               // 🇮🇳 Indian Aadhaar
  .pan()                   // 🇮🇳 Indian PAN
  .tfn()                   // 🇦🇺 Australian TFN
  .nric();                 // 🇸🇬 Singapore NRIC/FIN

// Generic validator with enum
ruleFor((x) => x.document)
  .nationalId(NationalIdType.esDni)
  .nationalId(NationalIdType.fromCountry('BR', 'CPF')!);

Custom Validators

// Simple predicate
ruleFor((x) => x.username)
  .must((username) => !_reservedNames.contains(username))
  .withMessage('Username is reserved');

// With object context
ruleFor((x) => x.password)
  .mustWith((password, user) => password != user.email)
  .withMessage('Password cannot be your email');

// Async validation
ruleFor((x) => x.email)
  .mustAsync((email) async {
    final exists = await _checkEmailExists(email);
    return !exists;
  }, 'Email already exists');

Conditional Validation

// Only validate if condition is true
ruleFor((x) => x.passport)
  .notEmpty()
  .when((user) => user.country != 'US');

// Skip validation if condition is true
ruleFor((x) => x.ssn)
  .notEmpty()
  .unless((user) => user.isInternational);

Nested Object Validation

class Address {
  final String? street;
  final String? city;
  Address({this.street, this.city});
}

class Person {
  final Address? address;
  Person({this.address});
}

class AddressValidator extends AbstractValidator<Address> {
  AddressValidator() {
    ruleFor((a) => a.street).notEmpty();
    ruleFor((a) => a.city).notEmpty();
  }
}

class PersonValidator extends AbstractValidator<Person> {
  PersonValidator() {
    ruleFor((p) => p.address)
      .setValidator(AddressValidator());
  }
}

Collection Validation

class Order {
  final List<OrderItem> items;
  Order({required this.items});
}

class OrderValidator extends AbstractValidator<Order> {
  OrderValidator() {
    // Validate the collection itself
    ruleFor((o) => o.items)
      .notEmpty()
      .withMessage('Order must have at least one item');
    
    // Validate each item in the collection
    ruleForEach((o) => o.items)
      .setValidator(OrderItemValidator());
  }
}

Inline Validation

Quick validation without creating validator classes:

// Single value
final result = 'john@example.com'.validateEmail();
if (result.isValid) {
  print('✅ Valid email');
}

// With custom message
final result = password.validateMinLength(8, 'Too short');

// Chain validations
final result = email
    .validateNotEmpty()
    .validateEmail()
    .validateMaxLength(100);

// Get value or throw
try {
  final validEmail = email.validateEmail().getOrThrow();
  print('Using: $validEmail');
} catch (e) {
  print('Invalid email');
}

Cascade Mode

Control how validation continues after errors:

// Stop on first error (default: Continue)
ruleFor((x) => x.password)
  .notEmpty()
  .minLength(8)
  .matches(RegExp(r'[A-Z]'))
  .cascadeMode(CascadeMode.stop);  // Stops at first failure

🚀 Isolate Validation API

Three methods for parallel processing:

// 1. Single object validation in isolate
final result = await validator.validateIsolate(user);

// 2. Batch validation (most efficient)
final results = await validator.validateManyIsolate(users);

// 3. Async validation in isolate
final result = await validator.validateAsyncIsolate(user);

Performance:

  • Overhead: ~5-10ms per isolate
  • Batch of 10,000 objects: ~150ms (vs ~1000ms synchronous)
  • Ideal for: CSV imports, Excel validation, API batch endpoints

📊 Supported Postal Codes

129 countries organized by region:

  • North America (12): US, CA, MX, CR, PA, GT, SV, HN, NI, CU, DO, PR
  • South America (10): BR, AR, CL, CO, PE, VE, EC, BO, PY, UY
  • Western Europe (18): GB, FR, DE, ES, IT, NL, BE, CH, AT, PT, IE, LU, MC, AD, SM, LI, VA, GI
  • Nordic Europe (7): SE, NO, DK, FI, IS, FO, GL
  • Eastern Europe (25): PL, CZ, SK, HU, RO, BG, HR, SI, EE, LV, LT, UA, BY, MD, RS, BA, MK, AL, GR, CY, MT, ME, XK
  • Russia & Central Asia (5): RU, KZ, AM, AZ, GE
  • East Asia (6): JP, CN, KR, TW, HK, MO
  • Southeast Asia (10): TH, VN, MY, SG, ID, PH, MM, KH, LA, BN
  • South Asia (7): IN, PK, BD, LK, NP, BT, MV
  • Middle East (13): TR, IL, SA, AE, QA, KW, BH, OM, JO, LB, IQ, IR, PS
  • Africa (13): ZA, EG, MA, DZ, TN, LY, KE, NG, ET, GH, MR, MU, LC
  • Oceania (5): AU, NZ, PG, FJ, VG

📈 Statistics

  • 56+ Validators built-in
  • 129 Countries postal code support
  • 200+ Tests (100% passing)
  • 93% Code Coverage
  • Zero Dependencies
  • Production Ready

🎯 Real-World Examples

CSV Import with Progress

Future<void> importCSV(File file) async {
  final lines = await file.readAsLines();
  final users = lines.skip(1).map((line) => User.fromCsv(line)).toList();
  
  print('Validating ${users.length} users...');
  
  // Validate in isolate (non-blocking)
  final results = await validator.validateManyIsolate(users);
  
  // Filter valid/invalid
  final valid = <User>[];
  final invalid = <(User, ValidationResult)>[];
  
  for (var i = 0; i < users.length; i++) {
    if (results[i].isValid) {
      valid.add(users[i]);
    } else {
      invalid.add((users[i], results[i]));
    }
  }
  
  await database.insertUsers(valid);
  print('✅ Imported: ${valid.length}');
  print('❌ Rejected: ${invalid.length}');
}

Flutter Form with Real-time Validation

class LoginForm extends StatefulWidget {
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _validator = LoginValidator();
  String _email = '';
  String _password = '';
  
  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: 'Email'),
            onChanged: (value) => setState(() => _email = value),
            validator: (_) {
              final result = _email.validateEmail();
              return result.isValid ? null : result.errors.first.errorMessage;
            },
          ),
          TextFormField(
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
            onChanged: (value) => setState(() => _password = value),
            validator: (_) {
              final result = _password.validateMinLength(8);
              return result.isValid ? null : result.errors.first.errorMessage;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                _login();
              }
            },
            child: Text('Login'),
          ),
        ],
      ),
    );
  }
}

Backend API Batch Validation

@Post('/users/batch')
Future<Response> createUsersBatch(List<UserDto> users) async {
  final validator = UserValidator();
  
  // Validate all users in parallel
  final results = await validator.validateManyIsolate(users);
  
  final validUsers = <UserDto>[];
  final errors = <Map<String, dynamic>>[];
  
  for (var i = 0; i < users.length; i++) {
    if (results[i].isValid) {
      validUsers.add(users[i]);
    } else {
      errors.add({
        'index': i,
        'user': users[i].toJson(),
        'errors': results[i].errors.map((e) => {
          'property': e.propertyName,
          'message': e.errorMessage,
        }).toList(),
      });
    }
  }
  
  await userRepository.createMany(validUsers);
  
  return Response.json({
    'created': validUsers.length,
    'failed': errors.length,
    'errors': errors,
  });
}

📖 Complete Examples

Check the /example folder for complete working examples:

  • basic_example.dart - Simple validation
  • advanced_example.dart - Complex validations with all features
  • isolate_validation_example.dart - Parallel processing examples
  • inline_validation_example.dart - Quick inline validations

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT License - see LICENSE file for details.

🙏 Acknowledgments

Inspired by FluentValidation for .NET


🇪🇸 Español

Una biblioteca de validación fluida para Dart inspirada en FluentValidation (.NET). Construye reglas de validación expresivas y fuertemente tipadas para tus aplicaciones Dart y Flutter con soporte para procesamiento paralelo mediante isolates.

✨ Características

  • API Fluida - Reglas de validación encadenables y legibles
  • Tipado seguro - Soporte completo para el sistema de tipos de Dart y null-safety
  • Validadores ricos - Más de 56 validadores integrados para escenarios comunes
  • Validadores avanzados - IP, MAC, GPS, Tarjeta de crédito, IBAN, ISBN, UUID, Códigos postales (129 países)
  • Validadores de Fecha/Hora - Pasado/Futuro, Validación de edad, Rangos de fechas
  • Documentos de Identidad - 26 validadores para 18 países (DNI, SSN, CPF, CURP, etc.)
  • Procesamiento paralelo - Validación basada en isolates para lotes grandes (1000+ objetos)
  • Validadores personalizados - Fácil agregar lógica de validación personalizada
  • Validación asíncrona - Soporte integrado para validación asíncrona
  • Reglas condicionales - Condiciones When/Unless para validación dinámica
  • Validación anidada - Valida grafos de objetos complejos
  • Validación de colecciones - Valida colecciones y sus elementos
  • Validación en línea - Validación rápida sin crear clases validadoras
  • Cero dependencias - Ligero sin dependencias externas
  • Universal - Funciona con Dart VM, Flutter, Web y backend

📦 Instalación

Añade just_validation a tu pubspec.yaml:

dependencies:
  just_validation: ^0.3.0

environment:
  sdk: '>=2.19.0 <4.0.0'

Luego ejecuta:

dart pub get
# o
flutter pub get

🚀 Inicio Rápido

import 'package:just_validation/just_validation.dart';

// Define tu modelo
class Usuario {
  final String? nombre;
  final String? email;
  final int? edad;

  Usuario({this.nombre, this.email, this.edad});
}

// Crea un validador
class ValidadorUsuario extends AbstractValidator<Usuario> {
  ValidadorUsuario() {
    ruleFor((usuario) => usuario.nombre)
        .notEmpty()
        .withMessage('El nombre es requerido')
        .minLength(2)
        .maxLength(50);
    
    ruleFor((usuario) => usuario.email)
        .notEmpty()
        .emailAddress()
        .withMessage('Dirección de email inválida');
    
    ruleFor((usuario) => usuario.edad)
        .greaterThanOrEqual(18)
        .withMessage('Debe ser mayor de 18 años')
        .lessThan(120)
        .when((usuario) => usuario.edad != null);
  }
}

// Usa el validador
void main() {
  final validador = ValidadorUsuario();
  final usuario = Usuario(nombre: 'Juan', email: 'juan@example.com', edad: 25);
  
  final resultado = validador.validate(usuario);
  
  if (resultado.isValid) {
    print('✅ Usuario válido!');
  } else {
    for (var error in resultado.errors) {
      print('❌ ${error.propertyName}: ${error.errorMessage}');
    }
  }
}

⚡ Procesamiento Paralelo con Isolates

Para lotes grandes o validaciones computacionalmente costosas, usa validación basada en isolates:

// Valida 10,000 objetos sin bloquear el hilo principal
final usuarios = List.generate(10000, (i) => Usuario(...));

// En lugar de:
// final resultados = usuarios.map((u) => validador.validate(u)).toList(); // Bloquea UI

// Usa isolates:
final resultados = await validador.validateManyIsolate(usuarios); // ¡No bloqueante!

print('✅ Validados ${resultados.length} usuarios en paralelo');

Cuándo usar isolates:

  • ✅ Procesamiento por lotes (1000+ objetos)
  • ✅ Validaciones complejas (50+ reglas)
  • ✅ No puede bloquear el hilo UI (apps Flutter)
  • ✅ Procesamiento en segundo plano

Cuándo NO usar isolates:

  • ❌ Formularios simples (1-100 campos)
  • ❌ Validaciones rápidas (<50ms total)
  • ❌ Lotes pequeños (<100 objetos)

📚 Documentación

Validadores de String

ruleFor((x) => x.nombre)
  .notEmpty()           // No debe estar vacío
  .notNull()            // No debe ser nulo
  .minLength(2)         // Longitud mínima
  .maxLength(50)        // Longitud máxima
  .length(5, 20)        // Longitud entre 5 y 20
  .matches(RegExp(r'^[a-zA-Z]+$'))  // Patrón regex
  .emailAddress()       // Email válido
  .url()                // URL válida

Validadores Numéricos

ruleFor((x) => x.edad)
  .greaterThan(0)            // > 0
  .greaterThanOrEqual(18)    // >= 18
  .lessThan(100)             // < 100
  .lessThanOrEqual(99)       // <= 99
  .inclusiveBetween(18, 65)  // Entre 18 y 65 (inclusivo)
  .exclusiveBetween(0, 100)  // Entre 0 y 100 (exclusivo)

Validadores de Comparación

ruleFor((x) => x.password)
  .equal('password_confirmado')      // Debe ser igual
  .notEqual('password_antiguo')      // No debe ser igual

Validadores Avanzados

// Dirección IP (IPv4, IPv6, o ambas)
ruleFor((x) => x.ip)
  .ipAddress()           // IPv4 o IPv6
  .ipv4Address()         // Solo IPv4
  .ipv6Address()         // Solo IPv6

// Dirección MAC
ruleFor((x) => x.mac)
  .macAddress()          // Soporta :, -, o sin separador

// Coordenadas GPS
ruleFor((x) => x.lat)
  .latitude()            // -90 a 90
ruleFor((x) => x.lng)
  .longitude()           // -180 a 180

// Tarjeta de Crédito (algoritmo Luhn)
ruleFor((x) => x.tarjeta)
  .creditCard()          // Visa, MC, Amex, etc.

// IBAN (52 países)
ruleFor((x) => x.iban)
  .iban()                // Validación completa Mod-97

// ISBN (10 o 13)
ruleFor((x) => x.isbn)
  .isbn()                // ISBN-10 o ISBN-13

// UUID (v1-v5)
ruleFor((x) => x.uuid)
  .uuid()                // Todas las versiones UUID

// Código Postal (129 países)
ruleFor((x) => x.cp)
  .postalCode('US')      // Códigos ZIP de EE.UU.
  .postalCode('ES')      // Códigos postales españoles
  .postalCode('MX')      // Códigos postales mexicanos
  // Soporta 129 países en todos los continentes

Validadores de Fecha/Hora

ruleFor((x) => x.fechaNacimiento)
  .isInPast()                    // Debe ser en el pasado
  .minAge(18)                    // Debe tener 18+ años
  .withMessage('Debe ser mayor de 18 años');

ruleFor((x) => x.fechaCita)
  .isInFuture()                  // Debe ser en el futuro
  .isBefore(DateTime(2025, 12, 31))  // Antes de fin de año
  .isAfter(DateTime.now());      // Después de hoy

ruleFor((x) => x.fechaEvento)
  .isBetween(fechaInicio, fechaFin) // Dentro del rango (inclusivo)
  .isToday()                     // Debe ser hoy
  .isSameDay(fechaReferencia);   // Mismo día que referencia

Validadores de Documentos de Identidad

// Documentos Europeos
ruleFor((x) => x.documento)
  .dni()                   // 🇪🇸 DNI Español
  .nie()                   // 🇪🇸 NIE Español
  .nin()                   // 🇬🇧 NIN Británico
  .germanId()              // 🇩🇪 Personalausweis Alemán
  .codiceFiscale()         // 🇮🇹 Codice Fiscale Italiano
  .nif()                   // 🇵🇹 NIF Portugués
  .bsn()                   // 🇳🇱 BSN Holandés
  .belgianNationalNumber() // 🇧🇪 Número Nacional Belga
  .pesel()                 // 🇵🇱 PESEL Polaco
  .frenchNationalId();     // 🇫🇷 CNI Francés

// Documentos Americanos
ruleFor((x) => x.documento)
  .ssn()                   // 🇺🇸 Social Security Number
  .curp()                  // 🇲🇽 CURP Mexicano
  .rfc()                   // 🇲🇽 RFC Mexicano
  .cpf()                   // 🇧🇷 CPF Brasileño
  .cnpj()                  // 🇧🇷 CNPJ Brasileño
  .cuit()                  // 🇦🇷 CUIT/CUIL Argentino
  .rut()                   // 🇨🇱 RUT Chileno
  .sin()                   // 🇨🇦 SIN Canadiense
  .colombianCc()           // 🇨🇴 Cédula Colombiana
  .ecuadorianCedula();     // 🇪🇨 Cédula Ecuatoriana

// Documentos Asiáticos
ruleFor((x) => x.documento)
  .chineseId()             // 🇨🇳 身份证 Chino (18 dígitos)
  .aadhaar()               // 🇮🇳 Aadhaar Indio
  .pan()                   // 🇮🇳 PAN Indio
  .tfn()                   // 🇦🇺 TFN Australiano
  .nric();                 // 🇸🇬 NRIC Singapurense

// Validador genérico con enum
ruleFor((x) => x.documento)
  .nationalId(NationalIdType.esDni)
  .nationalId(NationalIdType.fromCountry('BR', 'CPF')!);

Validadores Personalizados

// Predicado simple
ruleFor((x) => x.username)
  .must((username) => !_nombresReservados.contains(username))
  .withMessage('El nombre de usuario está reservado');

// Con contexto del objeto
ruleFor((x) => x.password)
  .mustWith((password, usuario) => password != usuario.email)
  .withMessage('La contraseña no puede ser tu email');

// Validación asíncrona
ruleFor((x) => x.email)
  .mustAsync((email) async {
    final existe = await _verificarEmailExiste(email);
    return !existe;
  }, 'El email ya existe');

Validación Condicional

// Solo validar si la condición es verdadera
ruleFor((x) => x.pasaporte)
  .notEmpty()
  .when((usuario) => usuario.pais != 'MX');

// Saltar validación si la condición es verdadera
ruleFor((x) => x.curp)
  .notEmpty()
  .unless((usuario) => usuario.esExtranjero);

Validación de Objetos Anidados

class Direccion {
  final String? calle;
  final String? ciudad;
  Direccion({this.calle, this.ciudad});
}

class Persona {
  final Direccion? direccion;
  Persona({this.direccion});
}

class ValidadorDireccion extends AbstractValidator<Direccion> {
  ValidadorDireccion() {
    ruleFor((d) => d.calle).notEmpty();
    ruleFor((d) => d.ciudad).notEmpty();
  }
}

class ValidadorPersona extends AbstractValidator<Persona> {
  ValidadorPersona() {
    ruleFor((p) => p.direccion)
      .setValidator(ValidadorDireccion());
  }
}

Validación de Colecciones

class Pedido {
  final List<ItemPedido> items;
  Pedido({required this.items});
}

class ValidadorPedido extends AbstractValidator<Pedido> {
  ValidadorPedido() {
    // Validar la colección en sí
    ruleFor((p) => p.items)
      .notEmpty()
      .withMessage('El pedido debe tener al menos un item');
    
    // Validar cada item en la colección
    ruleForEach((p) => p.items)
      .setValidator(ValidadorItemPedido());
  }
}

Validación en Línea

Validación rápida sin crear clases validadoras:

// Valor único
final resultado = 'juan@example.com'.validateEmail();
if (resultado.isValid) {
  print('✅ Email válido');
}

// Con mensaje personalizado
final resultado = password.validateMinLength(8, 'Demasiado corto');

// Encadenar validaciones
final resultado = email
    .validateNotEmpty()
    .validateEmail()
    .validateMaxLength(100);

// Obtener valor o lanzar excepción
try {
  final emailValido = email.validateEmail().getOrThrow();
  print('Usando: $emailValido');
} catch (e) {
  print('Email inválido');
}

Modo Cascada

Controla cómo continúa la validación después de errores:

// Detener en el primer error (por defecto: Continue)
ruleFor((x) => x.password)
  .notEmpty()
  .minLength(8)
  .matches(RegExp(r'[A-Z]'))
  .cascadeMode(CascadeMode.stop);  // Se detiene en el primer fallo

🚀 API de Validación con Isolates

Tres métodos para procesamiento paralelo:

// 1. Validación de un solo objeto en isolate
final resultado = await validador.validateIsolate(usuario);

// 2. Validación por lotes (más eficiente)
final resultados = await validador.validateManyIsolate(usuarios);

// 3. Validación asíncrona en isolate
final resultado = await validador.validateAsyncIsolate(usuario);

Rendimiento:

  • Overhead: ~5-10ms por isolate
  • Lote de 10,000 objetos: ~150ms (vs ~1000ms síncrono)
  • Ideal para: Importaciones CSV, validación de Excel, endpoints de lote en API

📊 Códigos Postales Soportados

129 países organizados por región:

  • América del Norte (12): US, CA, MX, CR, PA, GT, SV, HN, NI, CU, DO, PR
  • América del Sur (10): BR, AR, CL, CO, PE, VE, EC, BO, PY, UY
  • Europa Occidental (18): GB, FR, DE, ES, IT, NL, BE, CH, AT, PT, IE, LU, MC, AD, SM, LI, VA, GI
  • Europa Nórdica (7): SE, NO, DK, FI, IS, FO, GL
  • Europa Oriental (25): PL, CZ, SK, HU, RO, BG, HR, SI, EE, LV, LT, UA, BY, MD, RS, BA, MK, AL, GR, CY, MT, ME, XK
  • Rusia y Asia Central (5): RU, KZ, AM, AZ, GE
  • Asia Oriental (6): JP, CN, KR, TW, HK, MO
  • Sudeste Asiático (10): TH, VN, MY, SG, ID, PH, MM, KH, LA, BN
  • Asia del Sur (7): IN, PK, BD, LK, NP, BT, MV
  • Oriente Medio (13): TR, IL, SA, AE, QA, KW, BH, OM, JO, LB, IQ, IR, PS
  • África (13): ZA, EG, MA, DZ, TN, LY, KE, NG, ET, GH, MR, MU, LC
  • Oceanía (5): AU, NZ, PG, FJ, VG

📈 Estadísticas

  • Más de 56 validadores integrados
  • 129 países con soporte de códigos postales
  • 200+ tests (100% pasando)
  • 93% de cobertura de código
  • Cero dependencias
  • Listo para producción

📖 Ejemplos Completos

Revisa la carpeta /example para ejemplos completos funcionales:

  • basic_example.dart - Validación simple
  • advanced_example.dart - Validaciones complejas con todas las características
  • isolate_validation_example.dart - Ejemplos de procesamiento paralelo
  • inline_validation_example.dart - Validaciones rápidas en línea

🤝 Contribuir

¡Las contribuciones son bienvenidas! Por favor siéntete libre de enviar un Pull Request.

📄 Licencia

Licencia MIT - ver archivo LICENSE para detalles.

🙏 Agradecimientos

Inspirado en FluentValidation para .NET


Made with ❤️ for the Dart & Flutter community

Report Bug · Request Feature · Documentation

Libraries

just_validation
A fluent validation library for Dart inspired by FluentValidation.