jetleaf_validation 1.0.2
jetleaf_validation: ^1.0.2 copied to clipboard
Validation module for JetLeaf — provides data, input, and schema validation utilities.
✅ JetLeaf Validation — Data Validation & Constraints #
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
@Validfor 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 utilitiesjetleaf_logging— Structured loggingjetleaf_pod— Pod lifecyclejetleaf_core— Core frameworkjetleaf_env— Environment configuration
📄 License #
This package is part of the JetLeaf Framework. See LICENSE in the root directory.
🔗 Related Packages #
jetleaf_core— Framework integrationjetleaf_web— HTTP request validationjetson— JSON validation
📞 Support #
For issues, questions, or contributions, visit:
Created with ❤️ by Hapnium