Validation
A comprehensive guide to input validation in Khadem. Learn how to validate user input, create custom validation rules, use Form Request classes, and handle validation errors effectively.
Introduction
Validation is a critical component of any web application. Khadem provides a robust and intuitive validation system inspired by Laravel, allowing you to validate incoming HTTP requests with ease. The validation system is designed to be:
Easy to Use
Simple, expressive syntax that makes validation rules easy to read and write. Chain multiple rules with the pipe character.
Powerful
50+ built-in validation rules covering common use cases, from basic type checking to complex file validation.
Extensible
Create custom validation rules tailored to your application's specific business logic and requirements.
Type-Safe
Leverage Dart's type system with automatic type conversion and validation for a safer codebase.
Quick Start
The most common way to validate incoming requests in Khadem is by calling the validate() method on the request object. This method accepts a map of validation rules and automatically throws a ValidationException if validation fails, which you can catch and handle appropriately.
Here's a complete example of validating user registration data:
Basic Request Validation
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.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'email': 'This email address is already registered.',
},
'exception_type': 'ValidationException'
});
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.status(201).sendJson({
'success': true,
'message': 'User created successfully',
'data': {
'user': user.toJson(),
}
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} catch (e) {
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
}How It Works
- Validation Rules: Define rules using pipe-separated strings (e.g.,
'required|email|max:255') - Automatic Validation: The
validate()method automatically validates input and throwsValidationExceptionon failure - Type Safety: Validated data is returned as a
Map<String, dynamic>containing only the validated fields - Error Handling: Catch
ValidationExceptionto handle validation errors gracefully
Important Notes
- Validation rules follow Laravel-inspired syntax for familiarity
- Database validation rules like
uniqueandexistsmust be implemented manually - Custom messages can be provided to override default error messages
- Only validated fields are returned in the result
Available Validation Rules
Basic Rules
requiredField must be presentnullableField can be nullstringMust be a stringintMust be an integernumericMust be numericboolMust be booleanurlMust be valid URLactive_urlURL must be accessibleipMust be valid IPipv4Must be valid IPv4ipv6Must be valid IPv6Size & Comparison Rules
min:valueMinimum value/lengthmax:valueMaximum value/lengthalphaAlphabetic characters onlyalpha_numAlphanumeric characters onlyalpha_dashAlphanumeric, dashes, underscoresstarts_with:valMust start with specified valueends_with:valMust end with specified valueregex:patternMust match regex patternuuidMust be valid UUIDjsonMust be valid JSONphoneMust be valid phone numberDatabase Rules
unique:table,columnUnique in database (not implemented)exists:table,columnMust 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
dateMust be valid datedate_format:formatDate must match formatbefore:dateMust be before specified dateafter:dateMust be after specified dateArray & Collection Rules
arrayMust be an arraymin_items:NMinimum number of itemsmax_items:NMaximum number of itemsdistinctItems must be uniquein_array:fieldMust exist in specified arraynot_in_array:fieldMust not exist in specified arrayFile & Upload Rules
fileMust be a valid fileimageMust be an image filemimes:ext1,ext2Must have specified extensionsmax_file_size:NMaximum file size in KBConditional Rules
sometimesValidate only if presentrequired_if:field,valueRequired if other field has valueprohibitedField must not be presentprohibited_if:field,valueProhibited if other field has valueEmail & Confirmation Rules
emailMust be valid email addressconfirmedMust match field_confirmationin:val1,val2,val3Must be one of specified valuesValidation Tips & Best Practices
Combining Rules
Use the pipe | character to chain multiple rules:
'email': 'required|email|max:255' Nullable vs Optional
nullable allows null values, while omitting required makes a field optional.
Array Validation
Use .* notation to validate array items:
'tags.*': 'required|string|max:50' Rule Order Matters
Place nullable first, and type-checking rules (like int, string) before size rules.
Manual Validation
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.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'email': 'This email address is already registered.',
},
'exception_type': 'ValidationException'
});
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',
'data': {
'user': user.toJson(),
}
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} catch (e) {
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
}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 Messages
While Khadem provides sensible default error messages for all validation rules, you often want to provide more user-friendly, context-specific messages that match your application's tone and help users understand exactly what they need to fix. Custom messages make your validation errors more helpful and professional.
import 'package:khadem/khadem_dart.dart';
class UserController {
static Future store(Request req, Response res) async {
try {
// Validate with custom error messages
final validatedData = await req.validate(
{
'email': 'required|email',
'password': 'required|min:8|confirmed',
'age': 'required|int|min:18',
'profile_picture': 'nullable|image|max:2048',
'terms': 'required|accepted',
},
messages: {
// Pattern: 'field.ruleName' => 'Custom message'
'email.required': 'We need your email address to create your account',
'email.email': 'Please enter a valid email address (e.g., user@example.com)',
'password.required': 'A password is required to secure your account',
'password.min': 'Your password must be at least 8 characters long for security',
'password.confirmed': 'The password confirmation does not match. Please try again',
'age.required': 'Please provide your age to continue',
'age.int': 'Age must be a valid number',
'age.min': 'You must be at least 18 years old to register',
'profile_picture.image': 'Profile picture must be an image file (jpg, png, gif, etc.)',
'profile_picture.max': 'Profile picture size cannot exceed 2MB (2048KB)',
'terms.required': 'You must accept the terms and conditions to proceed',
'terms.accepted': 'Please check the box to accept our terms and conditions',
},
);
// Process validated data
final user = await User.create(validatedData);
res.status(201).sendJson({
'success': true,
'message': 'User created successfully',
'data': {'user': user.toJson()}
});
} on ValidationException catch (e) {
// Error response with custom messages
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors, // Will contain your custom messages
'exception_type': 'ValidationException'
});
}
}
}
// Example Error Response with Custom Messages:
// {
// "error": true,
// "message": "Validation failed.",
// "status_code": 422,
// "timestamp": "2025-10-21T16:30:15.123456",
// "details": {
// "email": "We need your email address to create your account",
// "password": "Your password must be at least 8 characters long for security",
// "age": "You must be at least 18 years old to register",
// "terms": "You must accept the terms and conditions to proceed"
// },
// "exception_type": "ValidationException"
// }Custom Message Pattern
Custom messages use a simple naming convention: field.ruleName
email.requiredCustom message for when email field fails required rulepassword.minCustom message for when password field fails min ruleage.intCustom message for when age field fails int ruleBest Practices for Custom Messages
- • Be specific: Tell users exactly what's wrong and how to fix it
- • Be friendly: Use a conversational tone that matches your brand
- • Provide examples: Show users what a valid value looks like
- • Be consistent: Use the same tone and style across all messages
- • Avoid technical jargon: Make messages understandable to all users
💡 Use Case Example
Instead of the generic message "The email field is required", you might say:
"We need your email address to send you order updates and account notifications"
This explains why the field is required, making users more likely to provide the information.
Custom Validation Rules
// 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
}
}// 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
For complex validation scenarios or when you want to reuse validation logic across multiple controllers, Khadem provides Form Request classes. These classes encapsulate validation rules, authorization logic, custom messages, and data preparation in a single, reusable component.
Form Requests follow a powerful lifecycle that gives you fine-grained control over the validation process:
Form Request Lifecycle
Authorization Check
The authorize() method is called first to check if the user has permission to make this request
Input Preparation
The prepareForValidation() method allows you to clean, normalize, or modify input before validation
Validation
Rules defined in rules() are applied using custom messages from messages()
Post-Validation Processing
The passedValidation() method is called after successful validation to transform validated data
Creating a Form Request Class
// lib/src/http/requests/create_user_request.dart
import 'package:khadem/khadem.dart';
class CreateUserRequest extends FormRequest {
@override
Map<String, String> rules() {
return {
'name': 'required|string|max:255',
'email': 'required|email',
'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.email': 'Please provide a valid email address',
'password.min': 'Password must be at least 8 characters',
'role.in': 'Invalid role selected',
};
}
@override
void prepareForValidation(Request request) {
// Called before validation - access request data here
// Use this to normalize or clean data before validation runs
}
@override
void passedValidation(Map<String, dynamic> validated) {
// Called after successful validation
// Modify the validated data directly
validated['password'] = Hash.make(validated['password']);
validated['created_at'] = DateTime.now().toIso8601String();
}
@override
bool authorize(Request request) {
// Check if user is authorized to make this request
return request.user()?.can('create-users') ?? false;
}
}Method Overview
rules()- Define validation rules (required)messages()- Custom error messages (optional)authorize()- Authorization logic (optional, defaults to true)prepareForValidation()- Pre-processing (optional)passedValidation()- Post-processing (optional)
Benefits
- • Keeps controllers clean and focused
- • Reusable validation logic
- • Easy to test independently
- • Type-safe validated data
- • Centralized authorization
Using Form Requests in Controllers
// lib/src/http/controllers/user_controller.dart
class UserController {
static Future store(Request req, Response res) async {
try {
// Create FormRequest instance and validate
final formRequest = CreateUserRequest();
final validatedData = await formRequest.validate(req);
// Authorization and validation automatically handled by FormRequest
// validatedData contains validated fields with passedValidation() modifications
// Manual database uniqueness check (if needed)
final existingUser = await User.where('email', validatedData['email']).first();
if (existingUser != null) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'email': 'This email address is already registered.',
},
'exception_type': 'ValidationException'
});
return;
}
final user = User(
name: validatedData['name'],
email: validatedData['email'],
password: validatedData['password'], // Already hashed in passedValidation()
role: validatedData['role'],
created_at: validatedData['created_at'], // Added in passedValidation()
);
await user.save();
res.status(201).sendJson({
'success': true,
'message': 'User created successfully',
'data': {
'user': user.toJson(),
}
});
} on UnauthorizedException catch (e) {
res.status(403).sendJson({
'error': true,
'message': e.message,
'status_code': 403,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': 'UnauthorizedException'
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
}
}
}Key Features of Form Requests
Centralized Validation
All validation logic for a specific action in one place
Custom Messages
Override default error messages with user-friendly alternatives
Authorization Built-in
Check permissions before validation even runs
Data Transformation
Modify data before and after validation automatically
Error Handling
class UserController {
static Future store(Request req, Response res) async {
try {
final validatedData = await req.validate({
'name': 'required|string|max:255',
'email': 'required|email',
'password': 'required|string|min:8',
});
// Manual database uniqueness check
final existingUser = await User.where('email', validatedData['email']).first();
if (existingUser != null) {
// Return custom validation error for duplicate email
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'email': 'This email address is already registered.',
},
'exception_type': 'ValidationException'
});
return;
}
// Process validated data
final user = User(
name: validatedData['name'],
email: validatedData['email'],
password: Hash.make(validatedData['password']),
);
await user.save();
// Success response
res.status(201).sendJson({
'success': true,
'message': 'User created successfully',
'data': {
'user': user.toJson(),
}
});
} on ValidationException catch (e) {
// Khadem automatically formats ValidationException as:
// {
// "error": true,
// "message": "Validation failed.",
// "status_code": 422,
// "timestamp": "2025-10-21T16:22:50.456153",
// "details": { "field": "error message" },
// "exception_type": "ValidationException"
// }
res.status(422).sendJson({
'error': true,
'message': e.message,
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} on UnauthorizedException catch (e) {
res.status(403).sendJson({
'error': true,
'message': e.message,
'status_code': 403,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': 'UnauthorizedException'
});
} catch (e) {
// Handle unexpected errors
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
} When validation fails, Khadem returns a standardized JSON error response with HTTP status code 422 Unprocessable Entity. This response includes detailed information about what went wrong, making it easy for frontend applications to display appropriate error messages to users.
Validation Error Response Format
{
"error": true,
"message": "Validation failed.",
"status_code": 422,
"timestamp": "2025-10-21T16:22:50.456153",
"details": {
"email": "The email field is required.",
"password": "The password must be at least 8 characters.",
"age": "The age must be at least 18."
},
"exception_type": "ValidationException"
}Response Fields Explained
errorAlways true for error responsesmessageA general error message describing the type of errorstatus_codeHTTP status code (422 for validation errors)timestampISO 8601 timestamp when the error occurreddetailsObject containing field-specific error messages (key = field name, value = error message)exception_typeThe type of exception that was thrownFrontend Integration Tip
The details object makes it easy to display field-specific errors in your frontend. Simply map each key to the corresponding form field and display the error message to the user.
Advanced Examples
Beyond basic field validation, Khadem supports advanced validation scenarios including conditional rules, array/nested data validation, and comprehensive file upload validation with size, type, and dimension checks.
Conditional Validation
Apply different validation rules based on the values of other fields. Perfect for forms where certain fields are only required when specific options are selected.
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
};
// 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);
// Manual database uniqueness check (excluding current user)
final existingUser = await User.where('email', validatedData['email'])
.where('id', '!=', req.user?.id ?? 0)
.first();
if (existingUser != null) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'email': 'This email address is already registered.',
},
'exception_type': 'ValidationException'
});
return;
}
// Process validated data...
res.sendJson({
'success': true,
'message': 'Profile updated successfully',
'data': {
'user': req.user?.toJson(),
}
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} catch (e) {
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
}Common Use Cases:
- • Password fields only required when actually changing password
- • Business information only required for business account types
- • Shipping address only required when different from billing address
- • Different validation rules for different user roles
Array Validation
Validate arrays and nested object structures with ease. Khadem supports validating entire arrays as well as individual items within arrays using dot notation.
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.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': {
'categories': 'One or more selected categories do not exist.',
},
'exception_type': 'ValidationException'
});
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...
final product = await Product.create(validatedData);
res.status(201).sendJson({
'success': true,
'message': 'Product created successfully',
'data': {
'product': product.toJson(),
}
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} catch (e) {
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
}Array Validation Patterns:
tagsValidates the entire array (e.g., required, min items, max items)tags.*Validates each item in the array (e.g., each tag must be a string)items.*.nameValidates nested properties within array objectsFile Upload Validation
Comprehensive file upload validation including file type (mimes), size limits, image dimensions, and more. Ensure uploaded files meet your application's requirements before processing.
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|image|mimes:jpeg,png,jpg|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',
'data': {
'document': document?.filename,
'avatar': avatar?.filename,
'images_count': images?.length ?? 0,
}
});
} on ValidationException catch (e) {
res.status(422).sendJson({
'error': true,
'message': 'Validation failed.',
'status_code': 422,
'timestamp': DateTime.now().toIso8601String(),
'details': e.errors,
'exception_type': 'ValidationException'
});
} catch (e) {
res.status(500).sendJson({
'error': true,
'message': 'An unexpected error occurred',
'status_code': 500,
'timestamp': DateTime.now().toIso8601String(),
'exception_type': e.runtimeType.toString()
});
}
}
}File Validation Rules:
fileEnsures the field contains an uploaded fileimageFile must be an image (jpg, jpeg, png, gif, bmp, svg, webp)mimes:jpg,pngRestricts allowed file types by extensionmax:2048Maximum file size in kilobytes (2048 = 2MB)dimensionsValidate image dimensions (min_width, max_width, min_height, max_height, ratio)