Validation
Learn how to validate user input and handle validation errors in Khadem
Input ValidationError HandlingForm RequestsCustom Rules
Quick Start
Basic Validation
dart
import 'package:khadem/khadem_dart.dart';
class UserController {
static Future store(Request req, Response res) async {
try {
// Validate request data (excluding database rules for now)
final validatedData = await req.validate({
'name': 'required|string|max:255',
'email': 'required|email', // Note: unique validation done manually below
'password': 'required|string|min:8|confirmed',
'age': 'nullable|int|min:18|max:120',
});
// Manual database uniqueness check
final existingUser = await User.where('email', validatedData['email']).first();
if (existingUser != null) {
res.statusCode(422).sendJson({
'success': false,
'errors': {'email': ['This email is already registered']},
'message': 'Validation failed'
});
return;
}
// Create user with validated data
final user = User(
name: validatedData['name'],
email: validatedData['email'],
password: Hash.make(validatedData['password']),
age: validatedData['age'],
);
await user.save();
res.statusCode(201).sendJson({
'success': true,
'message': 'User created successfully',
'user': user.toJson(),
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors,
'message': 'Validation failed'
});
} else {
res.statusCode(500).sendJson({
'success': false,
'message': 'Internal server error'
});
}
}
}
}
💡 Note: Validation rules follow Laravel-inspired syntax
⚡ Tip: Use req.validate()
for automatic validation with error responses
Available Validation Rules
Basic Rules
required
Field must be presentnullable
Field can be nullstring
Must be a stringint
Must be an integernumeric
Must be numericbool
Must be booleanurl
Must be valid URLactive_url
URL must be accessibleip
Must be valid IPipv4
Must be valid IPv4ipv6
Must be valid IPv6Size & Comparison Rules
min:value
Minimum value/lengthmax:value
Maximum value/lengthbetween:min,max
Between valuesalpha
Alphabetic characters onlyalpha_num
Alphanumeric characters onlyalpha_dash
Alphanumeric, dashes, underscoresstarts_with:val
Must start with specified valueends_with:val
Must end with specified valueregex:pattern
Must match regex patternuuid
Must be valid UUIDjson
Must be valid JSONphone
Must be valid phone numberDatabase Rules
unique:table,column
Unique in database (not implemented)exists:table,column
Must exist in database (not implemented)⚠️ Note: Database validation rules are not yet implemented in Khadem. For now, perform database uniqueness and existence checks manually in your controllers.
Date & Time Rules
date
Must be valid datedate_format:format
Date must match formatbefore:date
Must be before specified dateafter:date
Must be after specified dateArray & Collection Rules
array
Must be an arraymin_items:N
Minimum number of itemsmax_items:N
Maximum number of itemsdistinct
Items must be uniquein_array:field
Must exist in specified arraynot_in_array:field
Must not exist in specified arrayFile & Upload Rules
file
Must be a valid fileimage
Must be an image filemimes:ext1,ext2
Must have specified extensionsmax_file_size:N
Maximum file size in KBConditional Rules
sometimes
Validate only if presentnullable
Can be null, skips validationrequired_if:field,value
Required if another field has valueprohibited
Field must not be presentprohibited_if:field,value
Prohibited if another field has valueManual Validation
dart
import 'package:khadem/khadem_dart.dart';
class UserController {
static Future update(Request req, Response res, int id) async {
try {
// Get request body data
final bodyData = await req.body;
// Validate data manually (excluding database rules for now)
final validatedData = req.validateData(bodyData, {
'name': 'required|string|max:255',
'email': 'required|email', // Note: unique validation done manually below
});
// Manual database uniqueness check (excluding current user)
final existingUser = await User.where('email', validatedData['email'])
.where('id', '!=', id)
.first();
if (existingUser != null) {
res.statusCode(422).sendJson({
'success': false,
'errors': {'email': ['This email is already registered']},
'message': 'Validation failed'
});
return;
}
// Update user
final user = await User.find(id);
user.name = validatedData['name'];
user.email = validatedData['email'];
await user.save();
res.sendJson({
'success': true,
'message': 'User updated successfully',
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors,
'message': 'Validation failed'
});
} else {
res.statusCode(500).sendJson({
'success': false,
'message': 'Internal server error'
});
}
}
}
}
When to Use Manual Validation
- • Complex validation logic that can't be expressed with rules
- • Conditional validation based on other field values
- • Need more control over validation flow
- • Custom error handling requirements
Custom Validation Rules
dart
// lib/src/validation/strong_password_rule.dart
import 'package:khadem/src/contracts/validation/rule.dart';
class StrongPasswordRule implements Rule {
@override
String? validate(String field, dynamic value, String? arg, {required Map<String, dynamic> data}) {
if (value == null || value.isEmpty) {
return null; // Let required rule handle this
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'Password must contain at least one uppercase letter';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return 'Password must contain at least one lowercase letter';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return 'Password must contain at least one number';
}
return null; // Valid
}
}
dart
// lib/src/core/validation/validation_service_provider.dart
import 'package:khadem/src/core/validation/validation_rule_repository.dart';
import '../../validation/strong_password_rule.dart';
class ValidationServiceProvider {
static void registerCustomRules() {
// Register custom validation rule
ValidationRuleRepository.register('strong_password', () => StrongPasswordRule());
}
}
// Using the custom rule
class UserController {
static Future create(Request req, Response res) async {
try {
final validatedData = await req.validate({
'password': 'required|strong_password',
'email': 'required|email',
});
res.sendJson({
'success': true,
'message': 'User validated successfully'
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors
});
}
}
}
}
Custom Rule Benefits
- • Reusable validation logic across your application
- • Complex business rules validation
- • Consistent error messages
- • Easy to test and maintain
Form Request Classes
dart
// lib/src/http/requests/create_user_request.dart
import 'package:khadem/src/core/http/form_request.dart';
class CreateUserRequest extends FormRequest {
@override
Map<String, String> rules() {
return {
'name': 'required|string|max:255',
'email': 'required|email', // Note: unique validation done manually
'password': 'required|string|min:8|confirmed',
'role': 'required|in:admin,user,moderator',
};
}
@override
Map<String, String> messages() {
return {
'name.required': 'Please provide your name',
'email.unique': 'This email is already registered',
'password.min': 'Password must be at least 8 characters',
'role.in': 'Invalid role selected',
};
}
@override
void prepareForValidation() {
// Modify input before validation
if (input('name') != null) {
merge({'name': input('name').trim()});
}
}
@override
void passedValidation() {
// Called after successful validation
// You can modify the validated data here
merge({'password': Hash.make(input('password'))});
}
}
dart
// lib/src/http/controllers/user_controller.dart
class UserController {
static Future store(CreateUserRequest req, Response res) async {
// Manual database uniqueness check
final existingUser = await User.where('email', req.input('email')).first();
if (existingUser != null) {
res.statusCode(422).sendJson({
'success': false,
'errors': {'email': ['This email is already registered']},
'message': 'Validation failed'
});
return;
}
// Validation is automatically handled by FormRequest
final user = User(
name: req.input('name'),
email: req.input('email'),
password: req.input('password'), // Already hashed
role: req.input('role'),
);
await user.save();
res.statusCode(201).sendJson({
'success': true,
'message': 'User created successfully',
'user': user.toJson(),
});
}
}
// routes/web.dart
Route.post('/users', UserController.store, request: CreateUserRequest);
Form Request Features
- • Centralized validation logic
- • Custom error messages
- • Input preparation and sanitization
- • Authorization checks
- • Automatic validation on controller injection
Error Handling
dart
class UserController {
static Future store(Request req, Response res) async {
try {
final validatedData = await req.validate({
'name': 'required',
'email': 'required|email',
});
// Process validated data
final user = User(
name: validatedData['name'],
email: validatedData['email'],
);
await user.save();
res.statusCode(201).sendJson({
'success': true,
'message': 'User created successfully',
});
} catch (e) {
if (e is ValidationException) {
// Handle validation errors
res.statusCode(422).sendJson({
'success': false,
'message': 'Validation failed',
'errors': e.errors,
});
} else {
res.statusCode(500).sendJson({
'success': false,
'message': 'Internal server error'
});
}
}
}
}
Error Response Format
{
"error": true,
"message": "Validation failed",
"errors": {
"email": ["The email field is required"],
"password": ["The password must be at least 8 characters"]
},
"status_code": 422
}
Advanced Examples
Conditional Validation
dart
class UserController {
static Future updateProfile(Request req, Response res) async {
try {
final bodyData = await req.body;
final rules = {
'name': 'required|string|max:255',
'email': 'required|email', // Note: unique validation done manually below
};
// Manual database uniqueness check (excluding current user)
final existingUser = await User.where('email', req.input('email'))
.where('id', '!=', req.user?.id ?? 0)
.first();
if (existingUser != null) {
res.statusCode(422).sendJson({
'success': false,
'errors': {'email': ['This email is already registered']},
'message': 'Validation failed'
});
return;
}
// Add password validation only if provided
if (req.has('password') && req.input('password')?.isNotEmpty == true) {
rules['password'] = 'required|string|min:8|confirmed';
rules['password_confirmation'] = 'required';
}
// Add company validation for business users
if (req.input('account_type') == 'business') {
rules['company_name'] = 'required|string|max:255';
rules['tax_id'] = 'required|string|size:9';
}
final validatedData = req.validateData(bodyData, rules);
// Process validated data...
res.sendJson({
'success': true,
'message': 'Profile updated successfully',
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors,
'message': 'Validation failed'
});
}
}
}
}
Array Validation
dart
class ProductController {
static Future store(Request req, Response res) async {
try {
final validatedData = await req.validate({
'name': 'required|string|max:255',
'price': 'required|numeric|min:0',
'categories': 'required|array|min_items:1',
'categories.*': 'int', // Note: exists validation done manually below
'images': 'nullable|array|max_items:5',
'images.*': 'file|mimes:jpeg,png,jpg|max_file_size:2048',
'specifications': 'nullable|array',
'specifications.*.name': 'required|string|max:100',
'specifications.*.value': 'required|string|max:255',
});
// Manual database existence check for categories
final categoryIds = validatedData['categories'] as List;
for (final categoryId in categoryIds) {
final category = await Category.find(categoryId);
if (category == null) {
res.statusCode(422).sendJson({
'success': false,
'errors': {'categories': ['One or more selected categories do not exist']},
'message': 'Validation failed'
});
return;
}
}
// Data structure:
// {
// 'name': 'Product Name',
// 'price': 99.99,
// 'categories': [1, 2, 3],
// 'images': [/* uploaded files */],
// 'specifications': [
// {'name': 'Color', 'value': 'Red'},
// {'name': 'Size', 'value': 'Large'}
// ]
// }
// Process validated data...
res.statusCode(201).sendJson({
'success': true,
'message': 'Product created successfully',
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors,
'message': 'Validation failed'
});
}
}
}
}
File Upload Validation
dart
class FileUploadController {
static Future upload(Request req, Response res) async {
try {
final validatedData = await req.validate({
'document': 'required|file|mimes:pdf,doc,docx|max_file_size:5120', // 5MB
'images': 'nullable|array|max_items:3',
'images.*': 'file|mimes:jpeg,png,jpg,gif|max_file_size:2048', // 2MB each
'avatar': 'nullable|file|mimes:jpeg,png,jpg|dimensions:min_width=100,min_height=100|max_file_size:1024',
});
// Access uploaded files
final document = req.file('document');
final images = req.filesByName('images'); // Array of files
final avatar = req.file('avatar');
// Process files...
if (document != null) {
await document.saveTo('/uploads/documents/${document.filename}');
}
if (avatar != null) {
await avatar.saveTo('/uploads/avatars/user_${req.user?.id ?? 0}.jpg');
}
res.sendJson({
'success': true,
'message': 'Files uploaded successfully',
});
} catch (e) {
if (e is ValidationException) {
res.statusCode(422).sendJson({
'success': false,
'errors': e.errors,
'message': 'Validation failed'
});
}
}
}
}
Testing Validation
dart
// Test validation rules
void main() {
group('Validation Rules', () {
test('required rule works correctly', () {
final data = {'name': ''};
final rules = {'name': 'required'};
final validator = Validator(data, rules);
expect(validator.fails(), true);
expect(validator.errors, contains('name'));
});
test('email rule validates email format', () {
final data = {'email': 'invalid-email'};
final rules = {'email': 'required|email'};
final validator = Validator(data, rules);
expect(validator.fails(), true);
expect(validator.errors, contains('email'));
});
test('custom rule integration', () {
// Register custom rule for testing
ValidationRuleRepository.register('test_rule', () => TestRule());
final data = {'field': 'test'};
final rules = {'field': 'test_rule'};
final validator = Validator(data, rules);
expect(validator.passes(), true);
});
});
group('Request Validation', () {
test('request validation works', () async {
// Mock request with test data
final mockReq = MockRequest();
when(mockReq.validate({
'name': 'required',
'email': 'required|email',
})).thenAnswer((_) async => {
'name': 'John Doe',
'email': 'john@example.com',
});
final result = await mockReq.validate({
'name': 'required',
'email': 'required|email',
});
expect(result, isNotNull);
expect(result['name'], equals('John Doe'));
});
test('request validation throws on invalid data', () async {
final mockReq = MockRequest();
when(mockReq.validate({
'email': 'required|email',
})).thenThrow(ValidationException({
'email': ['The email field is required']
}));
expect(
() async => await mockReq.validate({'email': 'required|email'}),
throwsA(isA<ValidationException>())
);
});
});
}
Testing Strategies
- • Test validation rules with valid and invalid data
- • Test custom validation rules in isolation
- • Test form request validation and error messages
- • Test validation error responses
- • Test conditional validation logic