Define a custom validation function
You can provide custom validation functions to validate values. Luthor supports two types of custom validation:
- Single-value validation - Validates a single value in isolation
- Schema-aware validation - Validates a value with access to the entire schema data (cross-field validation)
Single-Value Custom Validation
For validating a single value independently, use a function that accepts an Object? and returns a bool.
The typedef for the custom validation function is:
typedef CustomValidator = bool Function(Object? value);import 'package:luthor/luthor.dart';
void main() { final validator = l.custom((value) => value == 42); print(validator.validateValue(42));}With Code Generation, we can use the @WithCustomValidator annotation to provide a custom validation function.
Note: The custom validation function should be either a top-level function or a static class method when using Code Generation. Anonymous functions are not supported due to Dart limitations.
import 'package:luthor/luthor.dart';import 'package:freezed_annotation/freezed_annotation.dart';
part 'custom_schema.freezed.dart';part 'custom_schema.g.dart';
bool customValidatorFn(Object? value) => value == 42;
@freezed@luthorabstract class CustomSchema with _$CustomSchema { const factory CustomSchema({ @WithCustomValidator(customValidatorFn) required int value, }) = _CustomSchema;
factory CustomSchema.fromJson(Map<String, dynamic> json) => _$CustomSchemaFromJson(json);}
void main() { print($CustomSchemaValidate({'value': 42}));}Schema-Aware Custom Validation (Cross-Field Validation)
For validating a field against other fields in the same schema, use schema-aware custom validation. This is perfect for scenarios like password confirmation, date range validation, or any validation that depends on multiple fields.
The typedef for schema custom validation is:
typedef SchemaCustomValidator = bool Function(Object? value, Map<String, Object?> data);import 'package:luthor/luthor.dart';
bool passwordsMatch(Object? value, Map<String, Object?> data) { return value == data['password'];}
bool maxAgeGreaterThanMin(Object? value, Map<String, Object?> data) { if (value is int && data['minAge'] is int) { return value > (data['minAge'] as int); } return false;}
void main() { final schema = l.schema({ 'password': l.string().min(8).required(), 'confirmPassword': l .string() .customWithSchema(passwordsMatch, message: 'Passwords must match') .required(), 'minAge': l.int().required(), 'maxAge': l .int() .customWithSchema( maxAgeGreaterThanMin, message: 'Max age must be greater than min age', ) .required(), });
final result = schema.validateSchema({ 'password': 'password123', 'confirmPassword': 'password123', 'minAge': 18, 'maxAge': 65, });
print(result); // Success}With Code Generation, use the @WithSchemaCustomValidator annotation for cross-field validation:
import 'package:luthor/luthor.dart';import 'package:freezed_annotation/freezed_annotation.dart';
part 'signup_form.freezed.dart';part 'signup_form.g.dart';
// Schema custom validation functionsbool passwordsMatch(Object? value, Map<String, Object?> data) { return value == data['password'];}
bool isGreaterThanMinAge(Object? value, Map<String, Object?> data) { if (value is int && data['minAge'] is int) { return value > (data['minAge'] as int); } return false;}
@luthor@freezedabstract class SignupForm with _$SignupForm { const factory SignupForm({ @IsEmail() required String email, @HasMin(8) required String password, @WithSchemaCustomValidator(passwordsMatch, message: 'Passwords must match') required String confirmPassword, required int minAge, @WithSchemaCustomValidator(isGreaterThanMinAge, message: 'Max age must be greater than min age') required int maxAge, }) = _SignupForm;
factory SignupForm.fromJson(Map<String, dynamic> json) => _$SignupFormFromJson(json);}
void main() { // Test with valid data final validResult = $SignupFormValidate({ 'password': 'password123', 'confirmPassword': 'password123', 'minAge': 18, 'maxAge': 65, });
print(validResult); // Success
// Test with invalid data final invalidResult = $SignupFormValidate({ 'password': 'password123', 'confirmPassword': 'different_password', // Doesn't match 'minAge': 65, 'maxAge': 18, // Less than minAge });
switch (invalidResult) { case SchemaValidationError(errors: final errors): // Use generated ErrorKeys for type-safe error access final confirmPasswordError = invalidResult.getError(SignupFormErrorKeys.confirmPassword); final maxAgeError = invalidResult.getError(SignupFormErrorKeys.maxAge);
print('Password confirmation error: $confirmPasswordError'); print('Max age error: $maxAgeError'); case SchemaValidationSuccess(): // Won't be reached break; }}Common Use Cases for Schema Custom Validation
Password Confirmation
bool passwordsMatch(Object? value, Map<String, Object?> data) { return value == data['password'];}Date Range Validation
bool endDateAfterStartDate(Object? value, Map<String, Object?> data) { if (value is DateTime && data['startDate'] is DateTime) { return value.isAfter(data['startDate'] as DateTime); } return false;}Conditional Required Fields
bool conditionallyRequired(Object? value, Map<String, Object?> data) { final shouldRequire = data['requireField'] as bool? ?? false; if (shouldRequire) { return value != null && value.toString().isNotEmpty; } return true; // Not required when condition is false}Numeric Comparisons
bool maxGreaterThanMin(Object? value, Map<String, Object?> data) { if (value is num && data['minValue'] is num) { return value > (data['minValue'] as num); } return false;}Best Practices
- Use descriptive function names -
passwordsMatchis clearer thancustomValidator1 - Handle type safety - Always check types before casting in your validation functions
- Provide custom messages - Use the
messageparameter for user-friendly error messages - Keep validators pure - Avoid side effects in validation functions
- Test thoroughly - Cross-field validation can be complex, so test all edge cases
Schema custom validation enables powerful cross-field validation scenarios while maintaining type safety and clean, readable code.