βœ… JetLeaf Validation β€” Data Validation & Constraints

pub package License Dart SDK

Comprehensive data validation framework with declarative constraints and custom validators for JetLeaf applications.

πŸ“‹ Overview

jetleaf_validation provides robust validation capabilities:

  • Declarative Constraints β€” Annotations for input validation
  • Built-in Validators β€” Common validation rules
  • Custom Validators β€” Domain-specific validation logic
  • Validation Groups β€” Selective constraint validation
  • Nested Validation β€” Validate object hierarchies
  • Collection Validation β€” Validate lists and maps
  • Error Messages β€” Customizable validation messages
  • Pod Integration β€” Auto-validate pod properties

πŸš€ Quick Start

Installation

dependencies:
  jetleaf_validation: ^1.0.0

Basic Validation

import 'package:jetleaf_validation/validation.dart';

class User {
  @NotNull(message: 'Username cannot be null')
  @Size(min: 3, max: 20, message: 'Username must be 3-20 characters')
  late String username;

  @Email(message: 'Invalid email address')
  late String email;

  @Range(min: 18, max: 120, message: 'Age must be 18-120')
  late int age;
}

void main() {
  final validator = Validator();
  
  final user = User();
  user.username = 'ab';  // Too short
  user.email = 'invalid-email';
  user.age = 15;  // Too young

  final violations = validator.validate(user);
  
  for (final violation in violations) {
    print('${violation.propertyPath}: ${violation.message}');
  }
  // Output:
  // username: Username must be 3-20 characters
  // email: Invalid email address
  // age: Age must be 18-120
}

πŸ“š Key Features

1. Built-in Constraints

Common validation annotations:

class Product {
  @NotNull()
  @NotEmpty(message: 'Product name required')
  String? name;

  @Size(min: 10, max: 500)
  String? description;

  @Min(value: 0)
  @Max(value: 100)
  int discount = 0;

  @Positive(message: 'Price must be positive')
  double? price;

  @Email()
  String? contactEmail;

  @Pattern(regex: r'^[A-Z0-9]{10}$', message: 'Invalid product code')
  String? productCode;

  @NotBlank(message: 'SKU cannot be blank')
  String? sku;
}

final validator = Validator();
final violations = validator.validate(product);

2. Custom Validators

Domain-specific validation logic:

import 'package:jetleaf_validation/validation.dart';

@Target({TargetKind.field})
class ValidPhoneNumber extends ConstraintValidator<PhoneNumber, String> {
  @override
  bool isValid(String? value, ConstraintValidatorContext context) {
    if (value == null || value.isEmpty) {
      return true;  // @NotNull handles nulls
    }

    final cleaned = value.replaceAll(RegExp('[^0-9]'), '');
    
    // Valid if it's 10 digits (after removing non-digits)
    if (cleaned.length != 10) {
      context.buildConstraintViolationWithTemplate(
        'Phone number must be 10 digits'
      ).addConstraintViolation();
      return false;
    }
    
    return true;
  }
}

@PhoneNumber()
String userPhone = '(555) 123-4567';

3. Validation Groups

Selective constraint validation:

class User {
  @NotNull(groups: [ValidationGroup.CREATE])
  @Size(min: 3, max: 20)
  late String username;

  @Email(groups: [ValidationGroup.CREATE, ValidationGroup.UPDATE])
  late String email;

  @NotNull(groups: [ValidationGroup.UPDATE])
  late String id;
}

enum ValidationGroup {
  CREATE,
  UPDATE,
}

// Validate only CREATE constraints
final violations = validator.validate(user, groups: [ValidationGroup.CREATE]);

// Validate only UPDATE constraints
final violations = validator.validate(user, groups: [ValidationGroup.UPDATE]);

4. Nested Validation

Validate object hierarchies:

class Address {
  @NotNull()
  @Size(min: 3)
  late String street;

  @NotNull()
  late String city;

  @Size(min: 5, max: 10)
  late String zipCode;
}

class Person {
  @NotNull()
  late String name;

  @Valid()  // Validate nested object
  late Address address;

  @Valid()  // Validate all items in collection
  late List<Address> previousAddresses;
}

final violations = validator.validate(person);
// Will recursively validate person, address, and previousAddresses

5. Collection Validation

Validate lists and maps:

class Team {
  @Size(min: 1, max: 50)
  List<@NotNull @Valid Member> members = [];

  @NotEmpty(message: 'Must have team lead')
  Member? teamLead;

  Map<String, @Valid Position> positions = {};
}

class Member {
  @NotNull()
  late String name;

  @Range(min: 0, max: 100)
  late int experience;
}

class Position {
  @NotNull()
  late String title;

  @Positive()
  late double salary;
}

final violations = validator.validate(team);

6. Integration with Pods

Automatic validation in services:

@Service()
class UserService {
  final Validator _validator;

  @Autowired
  UserService(this._validator);

  Future<void> createUser(User user) async {
    // Validate before processing
    final violations = _validator.validate(user, groups: [ValidationGroup.CREATE]);
    
    if (violations.isNotEmpty) {
      throw ValidationException(violations);
    }

    // Process user
    await _userRepository.save(user);
  }
}

7. REST Controller Integration

Validate request bodies:

@RestController('/api/users')
class UserController {
  final UserService _service;
  final Validator _validator;

  @Autowired
  UserController(this._service, this._validator);

  @PostMapping('/')
  Future<HttpResponse> createUser(
    @RequestBody User user,
  ) async {
    // Validate input
    final violations = _validator.validate(user, groups: [ValidationGroup.CREATE]);
    
    if (violations.isNotEmpty) {
      return HttpResponse.badRequest({
        'error': 'Validation failed',
        'violations': violations.map((v) => {
          'field': v.propertyPath,
          'message': v.message,
        }).toList(),
      });
    }

    await _service.createUser(user);
    return HttpResponse.created(user);
  }
}

πŸ“– Built-in Constraints

Constraint Target Purpose
@NotNull Any Value cannot be null
@NotEmpty Collections, String Value cannot be empty
@NotBlank String String cannot be blank
@Size(min, max) Collections, String Size constraints
@Min(value) Number Minimum value
@Max(value) Number Maximum value
@Range(min, max) Number Range constraints
@Positive Number Must be > 0
@Negative Number Must be < 0
@Email String Valid email format
@Pattern(regex) String Regex pattern match
@Valid Object Nested validation

🎯 Common Patterns

Pattern 1: Form Validation

class RegistrationForm {
  @NotNull()
  @Size(min: 3, max: 20)
  late String username;

  @NotNull()
  @Email()
  late String email;

  @NotNull()
  @Size(min: 8, message: 'Password must be at least 8 characters')
  late String password;

  @NotNull()
  @AssertTrue(message: 'Must agree to terms')
  late bool agreedToTerms;
}

@RestController('/auth')
class AuthController {
  final Validator _validator;

  @PostMapping('/register')
  Future<HttpResponse> register(@RequestBody RegistrationForm form) async {
    final violations = _validator.validate(form);
    if (violations.isNotEmpty) {
      return HttpResponse.badRequest({
        'errors': violations.map((v) => v.message).toList(),
      });
    }

    // Proceed with registration
    return HttpResponse.ok({'status': 'registered'});
  }
}

Pattern 2: Business Rule Validation

class Order {
  @NotNull()
  late String customerId;

  @NotEmpty(message: 'Order must have items')
  late List<OrderItem> items;

  @Range(min: 0)
  late double totalAmount;

  @AssertTrue(message: 'Total must match item sum')
  bool isTotalCorrect() {
    final sum = items.fold<double>(
      0,
      (sum, item) => sum + item.price,
    );
    return (sum - totalAmount).abs() < 0.01;
  }
}

⚠️ Common Issues

Issue Cause Solution
Validation not running Validator not called Explicitly call validator.validate()
Nested validation skipped Missing @Valid Add @Valid annotation to nested objects
Custom validator not used Not registered Register with validator factory
Message not customized Default message used Add message parameter to constraint

πŸ“‹ Best Practices

βœ… DO

  • Define validation constraints close to fields
  • Use validation groups for different operations
  • Provide meaningful error messages
  • Validate early in request processing
  • Test validators independently
  • Use @Valid for nested objects
  • Create custom validators for business rules

❌ DON'T

  • Perform heavy validation in constructors
  • Mix validation with business logic
  • Ignore validation violations
  • Create overly complex validators
  • Share validator instances unsafely
  • Forget to validate nested collections

πŸ“¦ Dependencies

  • jetleaf_lang β€” Language utilities
  • jetleaf_logging β€” Structured logging
  • jetleaf_pod β€” Pod lifecycle
  • jetleaf_core β€” Core framework
  • jetleaf_env β€” Environment configuration

πŸ“„ License

This package is part of the JetLeaf Framework. See LICENSE in the root directory.

  • jetleaf_core β€” Framework integration
  • jetleaf_web β€” HTTP request validation
  • jetson β€” JSON validation

πŸ“ž Support

For issues, questions, or contributions, visit:


Created with ❀️ by Hapnium

Libraries

jetleaf_validation
βœ… JetLeaf Validation Library