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

requiredField must be present
nullableField can be null
stringMust be a string
intMust be an integer
numericMust be numeric
boolMust be boolean
urlMust be valid URL
active_urlURL must be accessible
ipMust be valid IP
ipv4Must be valid IPv4
ipv6Must be valid IPv6

Size & Comparison Rules

min:valueMinimum value/length
max:valueMaximum value/length
between:min,maxBetween values
alphaAlphabetic characters only
alpha_numAlphanumeric characters only
alpha_dashAlphanumeric, dashes, underscores
starts_with:valMust start with specified value
ends_with:valMust end with specified value
regex:patternMust match regex pattern
uuidMust be valid UUID
jsonMust be valid JSON
phoneMust be valid phone number

Database 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 date
date_format:formatDate must match format
before:dateMust be before specified date
after:dateMust be after specified date

Array & Collection Rules

arrayMust be an array
min_items:NMinimum number of items
max_items:NMaximum number of items
distinctItems must be unique
in_array:fieldMust exist in specified array
not_in_array:fieldMust not exist in specified array

File & Upload Rules

fileMust be a valid file
imageMust be an image file
mimes:ext1,ext2Must have specified extensions
max_file_size:NMaximum file size in KB

Conditional Rules

sometimesValidate only if present
nullableCan be null, skips validation
required_if:field,valueRequired if another field has value
prohibitedField must not be present
prohibited_if:field,valueProhibited if another field has value

Manual 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

On this page