--- url: /validasi/v0/types/array.md --- # Array The `Array` schema is used to validate the input value as an array or `List`. This schema contains some useful built-in validators to validate the input value. The following code shows how to create an `Array` schema: ::: code-group ```dart [Using Validasi] Validasi.array(Validasi.string()); ``` ```dart [Using Direct Class] ArrayValidator(StringValidator()); ``` ::: The `Array` schema requires a schema parameter to validate the items in the array. The schema parameter is used to validate each item in the array. You can also have nesting schemas in the `Array` schema. The following code shows how to create a nested `Array` schema: ```dart Validasi.array(Validasi.array(Validasi.string())); ``` ::: info The Array schema will reconstruct your input value accoring to the schema you provided. This allow you to take advantage of transformer in the schema to convert the array value into the desired format. ```dart Validasi.array(Validasi.number(transformer: NumberTransformer())); ``` Above code will convert the input value into a list of numbers. ::: ::: warning Notice about `null` value. When the item on your array fail to validate, the item will be replaced with `null` value. This is to ensure the array length is still the same as the input value. ```dart final schema = Validasi.array(Validasi.string()); final result = schema.tryParse(['a', 'b', 1]); print(result.value); // ['a', 'b', null] ``` If this behaviour is not what you want, you might want to use `parse` variants instead (E.g. `parse` and `parseAsync`) to prevent the validator returning result when error occurred. ::: Below are the available methods for the `Array` schema: \[\[toc]] ## min ```dart Validasi.array(Validasi.string()).min(int length, {String? message}); ``` The `min` method is used to validate the minimum number of items in the array. This method will return an error message if the array contains fewer items than the specified length. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.array(Validasi.string()).min(3); final result = schema.tryParse(['a', 'b']); print(result.errors.first.message); // 'field must have at least 3 items' } ``` ## max ```dart Validasi.array(Validasi.string()).max(int length, {String? message}); ``` The `max` method is used to validate the maximum number of items in the array. This method will return an error message if the array contains more items than the specified length. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.array(Validasi.string()).max(3); final result = schema.tryParse(['a', 'b', 'c', 'd']); print(result.errors.first.message); // 'field must have at most 3 items' } ``` ## contains ```dart Validasi.array(Validasi.string()).contains(List value, {String? message}); ``` The `contains` method is used to validate that the array contains the specified value. This method will return an error if the array contains any value that is not in the specified list. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.array(Validasi.string()).contains(['a']); final result = schema.tryParse(['b', 'c']); print(result.errors.first.message); // 'field must contain a' } ``` ## notContains ```dart Validasi.array(Validasi.string()).notContains(List value, {String? message}); ``` The `notContains` method is used to validate that the array does not contain the specified value. This method will return an error message if the array contains the specified value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.array(Validasi.string()).notContains(['a']); final result = schema.tryParse(['a', 'b', 'c']); print(result.errors.first.message); // 'field must not contain a' } ``` ## unique ```dart Validasi.array(Validasi.string()).unique({String? message}); ``` The `unique` method is used to validate that all items in the array are unique. This method will return an error message if the array contains duplicate items. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.array(Validasi.string()).unique(); final result = schema.tryParse(['a', 'b', 'a']); print(result.errors.first.message); // 'field must have unique items' } ``` --- --- url: /validasi/v0/guide/basic-concept.md --- # Basic Concept Before reading this, make sure you have read the [Execution Flow](/v0/guide/execution-order) to understand how the validation works in Validasi. ## Required by default Validasi takes inspiration from [Zod](https://zod.dev). Where by default, all fields are required. If you want to make a field to optional you need to use `nullable` method. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().nullable(); final result = schema.tryParse(null); print(result.isValid); // true } ``` By adding `nullable` method, the field is now optional and every rule after `nullable` will be ignored if the field is `null`. Except for Custom Rule. ## Validate Anything Validasi can validate anything, from simple string, number, object, array, and even nested object. Each of these validation are checked on the Runtime by the library. This means you can validate anything and Validasi will handle the type mismatch for you. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string(); final result = schema.tryParse(10); print(result.errors.first.message); // 'Expected type String. Got int instead.' } ``` This also means that the Dart Type System will be ignored when performing validation. It will be handled by Validasi instead. ## The Transformer Since Validasi is a runtime validation library, it can also transform the input value to the desired type. This is useful when your input is a numerical string and you want to convert it to an integer, where you can use the number validators later on. You can use the `transformer` parameter to provide a transformer to the suppported schema. > See [Transformer](/v0/guide/transformer) for more information. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number(transformer: NumberTransformer()); final result = schema.tryParse('10'); print(result.value); // 10 } ``` ## Custom Rule You can also create a custom rule by using `custom` method. This method will receive a function that will be called when the validation is performed. > See [Custom Rule](/v0/guide/custom-rule) for more information. ```dart import 'package:validasi/validasi.dart'; import 'package:app/data.dart'; void main() { final schema = Validasi.string().custom((value, fail) { if (Data.user().contains(value)) { return true; } return fail('Ups, user not found'); }); final result = schema.tryParse('world'); } ``` ## Throw on Error In order to throw an error when the validation failed, you can use `parse` variants method. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string(); try { final result = schema.parse(10); // or parseAsync } catch (e) { print(e); // 'Expected type String. Got int instead.' } } ``` ## Replacing default path By default, the error path is `field`. You can replace the default assign path to the parse method. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().minLength(3).maxLength(3); final result = schema.tryParse('', path: 'id'); // or parse(10, path: 'id'). final firstErr = result.errors.first; print(firstErr.path); // 'id' print(firstErr.message); // 'id must contains at least 3 characters.' } ``` The same applies to async variants (e.g `tryParseAsync`, `parseAsync`). ## Replacing default message You can also replace the default message by using the `message` parameter. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().minLength(3, message: 'The :name is too short'); final result = schema.tryParse('', path: 'id'); print(result.errors.first.message); // 'The id is too short' } ``` ::: info Notice `:name` this are special template keyword in which will be replaced by the path name. ::: ## Error Handling In Validasi, only `FieldError` will be intercept and handled by Validasi. Any other error will be thrown as is even though you run it in `tryParse` or `tryParseAsync`. The design is to make sure that you wary about other error beyond validation is occured and not handled by Validasi. --- --- url: /validasi/guide/rules.md --- # Built-in Rules Validasi comes with a set of universal validation rules that work across all data types. These modifier rules control validation behavior, allow transformations, and enable custom logic. ## Overview Built-in rules are divided into two categories: ### Global Modifier Rules Universal rules that modify validation behavior (covered on this page): * **Nullable** - Allow null values * **Required** - Ensure non-null values * **Transform** - Transform values during validation * **Having** - Custom validation with context access * **InlineRule** - Create custom validation rules inline ### Type-Specific Rules Rules designed for specific data types (see dedicated pages): * [String Rules](/rules/string) - `minLength`, `maxLength`, `oneOf` * [Number Rules](/rules/number) - `finite`, `lessThan`, `moreThan`, etc. * [Iterable Rules](/rules/iterable) - `minLength`, `forEach` * [Map Rules](/rules/map) - `hasFields`, `hasFieldKeys`, `conditionalField` *** ## Global Modifier Rules These rules work with any schema type and modify how validation behaves: ### Nullable Allows null values to pass validation. By default, Validasi rejects null values unless explicitly allowed. ```dart import 'package:validasi/rules.dart'; final optionalNameSchema = Validasi.string([ Nullable(), StringRules.minLength(3), ]); print(optionalNameSchema.validate(null).isValid); // true print(optionalNameSchema.validate('John').isValid); // true print(optionalNameSchema.validate('Jo').isValid); // false ``` **Use cases:** * Optional form fields * Optional API parameters * Nullable database columns **Examples with different types:** ```dart // Optional number final optionalAge = Validasi.number([ Nullable(), NumberRules.moreThanEqual(0), ]); // Optional list final optionalTags = Validasi.list([ Nullable(), IterableRules.minLength(1), ]); // Optional map final optionalSettings = Validasi.map([ Nullable(), MapRules.hasFieldKeys({'theme'}), ]); ``` ::: tip Best Practice Place `Nullable()` as the first rule in your schema to make the intent clear. ::: ### Required Explicitly ensures value is not null. This is the default behavior, but can be used for clarity. ```dart final requiredNameSchema = Validasi.string([ Required(), StringRules.minLength(3), ]); print(requiredNameSchema.validate(null).isValid); // false print(requiredNameSchema.validate('John').isValid); // true ``` **When to use:** * For explicit documentation of required fields * When overriding nullable schemas * In schema composition for clarity ### Transform Transforms the value during validation. The transformed value is used for subsequent rules and returned in the result. ```dart final trimmedSchema = Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(3), ]); final result = trimmedSchema.validate(' hello '); print(result.data); // "hello" (trimmed) print(result.isValid); // true ``` **Common transformations:** ```dart // Trim and lowercase final emailSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), StringRules.minLength(5), ]); // Convert to absolute value final absoluteSchema = Validasi.number([ Transform((value) => value?.abs()), NumberRules.moreThan(0), ]); // Filter empty strings from list final cleanListSchema = Validasi.list([ Transform((list) => list?.where((s) => s.isNotEmpty).toList()), IterableRules.minLength(1), ]); ``` ::: warning Order Matters Place `Transform` rules before validation rules to ensure the transformed value is validated. ::: **See also:** [Transformations Guide](/guide/transformations) for detailed transformation patterns. ### Having Custom validation with access to the validation context. Useful for complex validation logic that needs information about the validation state. ```dart final schema = Validasi.string([ Having((context, value) { // Access validation context // context provides information about the validation state if (value == null || value.isEmpty) { return 'Value is required'; } return null; // null means valid }), ]); ``` **Advanced usage:** ```dart final passwordConfirmSchema = Validasi.map([ MapRules.hasFields({ 'password': Validasi.string([StringRules.minLength(8)]), 'confirm_password': Validasi.string([StringRules.minLength(8)]), }), Having((context, value) { // Access the entire map being validated if (value is Map) { if (value['password'] != value['confirm_password']) { return 'Passwords do not match'; } } return null; }), ]); ``` **When to use `Having`:** * Need access to validation context * Cross-field validation * Complex conditional logic * State-dependent validation ### InlineRule Create custom validation rules inline without accessing context. Simpler than `Having` for basic custom validation. ```dart final passwordSchema = Validasi.string([ StringRules.minLength(8), InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain at least one uppercase letter'; } return null; // null means valid }), InlineRule((value) { if (!value.contains(RegExp(r'[0-9]'))) { return 'Must contain at least one number'; } return null; }), ]); ``` **Common patterns:** ```dart // Email validation final emailSchema = Validasi.string([ InlineRule((value) { if (!value.contains('@') || !value.contains('.')) { return 'Invalid email format'; } return null; }), ]); // URL validation final urlSchema = Validasi.string([ InlineRule((value) { if (!value.startsWith('http://') && !value.startsWith('https://')) { return 'URL must start with http:// or https://'; } return null; }), ]); // Custom business logic final ageSchema = Validasi.number([ InlineRule((value) { if (value < 18) { return 'Must be 18 or older'; } if (value > 120) { return 'Age seems invalid'; } return null; }), ]); // Pattern matching final usernameSchema = Validasi.string([ InlineRule((value) { if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { return 'Username can only contain letters, numbers, and underscores'; } return null; }), ]); ``` **Multiple conditions:** ```dart final strongPasswordSchema = Validasi.string([ StringRules.minLength(8), InlineRule((value) { final hasUpper = value.contains(RegExp(r'[A-Z]')); final hasLower = value.contains(RegExp(r'[a-z]')); final hasDigit = value.contains(RegExp(r'[0-9]')); final hasSpecial = value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); if (!hasUpper) return 'Must contain uppercase letter'; if (!hasLower) return 'Must contain lowercase letter'; if (!hasDigit) return 'Must contain number'; if (!hasSpecial) return 'Must contain special character'; return null; }), ]); ``` ## Combining Modifier Rules Modifier rules can be combined for powerful validation: ```dart final schema = Validasi.string([ Nullable(), // 1. Allow null Transform((value) => value?.trim()), // 2. Clean data Transform((value) => value?.toLowerCase()), // 3. Normalize InlineRule((value) { // 4. Custom validation if (value != null && value.length < 3) { return 'Too short'; } return null; }), StringRules.maxLength(50), // 5. Type-specific rules ]); ``` ## Best Practices ### 1. Order Your Rules Logically ```dart final schema = Validasi.string([ Nullable(), // 1. Handle nulls first Required(), // 2. Or require non-null Transform(...), // 3. Transform data InlineRule(...), // 4. Custom validation StringRules.minLength(3), // 5. Type-specific rules ]); ``` ### 2. Use the Right Tool ```dart // Simple custom validation → InlineRule InlineRule((value) => value.contains('@') ? null : 'Invalid email') // Need context → Having Having((context, value) => /* access context */) // Data cleaning → Transform Transform((value) => value?.trim()) ``` ### 3. Keep Rules Focused ```dart // Good ✓ - Each rule has one purpose final schema = Validasi.string([ Transform((value) => value?.trim()), InlineRule((value) { if (!value.contains('@')) return 'Must contain @'; return null; }), ]); // Less clear ✗ - Mixing concerns final schema2 = Validasi.string([ Transform((value) { final trimmed = value?.trim(); if (trimmed != null && !trimmed.contains('@')) { // Don't validate in Transform! } return trimmed; }), ]); ``` ### 4. Provide Clear Error Messages ```dart // Good ✓ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Password must contain at least one uppercase letter'; } return null; }) // Unclear ✗ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Invalid'; } return null; }) ``` ## Type-Specific Rules For rules specific to data types, see the dedicated pages: ## Modifier Rules Special rules that modify validation behavior: ### Nullable Allows null values to pass validation. ```dart import 'package:validasi/rules.dart'; final optionalNameSchema = Validasi.string([ Nullable(), StringRules.minLength(3), ]); print(optionalNameSchema.validate(null).isValid); // true print(optionalNameSchema.validate('John').isValid); // true print(optionalNameSchema.validate('Jo').isValid); // false ``` ### Required Ensures value is not null (default behavior, explicit form). ```dart final requiredNameSchema = Validasi.string([ Required(), StringRules.minLength(3), ]); ``` ### Transform Transforms the value during validation. ```dart final trimmedSchema = Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(3), ]); final result = trimmedSchema.validate(' hello '); print(result.data); // "hello" (trimmed) ``` See [Transformations](/guide/transformations) for more details. ### Having Custom validation with context access. ```dart final schema = Validasi.string([ Having((context, value) { // Access validation context // Implement custom logic return null; // or error message }), ]); ``` ### InlineRule Create custom validation rules inline. ```dart final passwordSchema = Validasi.string([ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain uppercase letter'; } return null; }), ]); ``` ## Next Steps * [Transformations](/guide/transformations) - Transform data during validation * [Error Handling](/guide/error-handling) - Work with validation errors * [Examples](/examples/string-validation) - See practical examples --- --- url: /validasi/examples/complex-structures.md --- # Complex Structures Examples Learn how to validate complex, nested data structures by combining Validasi's schema types. ## E-Commerce Examples ### Shopping Cart ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final cartSchema = Validasi.map([ MapRules.hasFields({ 'user_id': Validasi.number([ NumberRules.moreThan(0), ]), 'items': Validasi.list>([ IterableRules.minLength(1, message: 'Cart cannot be empty'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'product_id': Validasi.number([ NumberRules.moreThan(0), ]), 'name': Validasi.string([ StringRules.minLength(1), ]), 'price': Validasi.number([ NumberRules.moreThan(0.0), ]), 'quantity': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(99), ]), }), ]), ), ]), 'shipping': Validasi.map([ MapRules.hasFields({ 'address': Validasi.string([StringRules.minLength(10)]), 'city': Validasi.string([StringRules.minLength(2)]), 'postal_code': Validasi.string([StringRules.minLength(3)]), 'country': Validasi.string([StringRules.minLength(2)]), }), ]), 'payment_method': Validasi.string([ StringRules.oneOf(['card', 'paypal', 'bank_transfer']), ]), }), ]); final cart = { 'user_id': 123, 'items': [ { 'product_id': 1, 'name': 'Wireless Mouse', 'price': 29.99, 'quantity': 2, }, { 'product_id': 2, 'name': 'USB Cable', 'price': 9.99, 'quantity': 3, }, ], 'shipping': { 'address': '123 Main Street, Apt 4B', 'city': 'San Francisco', 'postal_code': '94102', 'country': 'USA', }, 'payment_method': 'card', }; final result = cartSchema.validate(cart); print('Valid: ${result.isValid}'); ``` ### Product Catalog ```dart final catalogSchema = Validasi.map([ MapRules.hasFields({ 'categories': Validasi.list>([ IterableRules.minLength(1), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'name': Validasi.string([StringRules.minLength(1)]), 'slug': Validasi.string([ Transform((value) => value?.toLowerCase().replaceAll(' ', '-')), ]), 'products': Validasi.list>([ IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'name': Validasi.string([StringRules.minLength(1)]), 'price': Validasi.number([NumberRules.moreThan(0)]), 'in_stock': Validasi.any(), 'images': Validasi.list([ IterableRules.minLength(1), ]), 'variants': Validasi.list>([ Nullable(), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'size': Validasi.string([Nullable()]), 'color': Validasi.string([Nullable()]), 'price_modifier': Validasi.number([ Nullable(), ]), }), ]), ), ]), }), ]), ), ]), }), ]), ), ]), }), ]); final catalog = { 'categories': [ { 'id': 1, 'name': 'Electronics', 'slug': 'electronics', 'products': [ { 'id': 101, 'name': 'Wireless Headphones', 'price': 99.99, 'in_stock': true, 'images': ['img1.jpg', 'img2.jpg'], 'variants': [ {'size': null, 'color': 'black', 'price_modifier': 0.0}, {'size': null, 'color': 'white', 'price_modifier': 5.0}, ], }, ], }, ], }; print(catalogSchema.validate(catalog).isValid); ``` ## Social Media Examples ### User Profile ```dart final profileSchema = Validasi.map([ MapRules.hasFields({ 'user': Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'username': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(3), StringRules.maxLength(20), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), InlineRule((value) { return value.contains('@') ? null : 'Invalid email'; }), ]), 'bio': Validasi.string([ Nullable(), StringRules.maxLength(500), ]), 'avatar_url': Validasi.string([Nullable()]), }), ]), 'stats': Validasi.map([ MapRules.hasFields({ 'followers': Validasi.number([NumberRules.moreThanEqual(0)]), 'following': Validasi.number([NumberRules.moreThanEqual(0)]), 'posts': Validasi.number([NumberRules.moreThanEqual(0)]), }), ]), 'recent_posts': Validasi.list>([ IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'content': Validasi.string([ StringRules.minLength(1), StringRules.maxLength(280), ]), 'likes': Validasi.number([NumberRules.moreThanEqual(0)]), 'comments': Validasi.number([NumberRules.moreThanEqual(0)]), 'created_at': Validasi.string([StringRules.minLength(1)]), 'media': Validasi.list>([ Nullable(), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'type': Validasi.string([ StringRules.oneOf(['image', 'video', 'gif']), ]), 'url': Validasi.string([StringRules.minLength(1)]), }), ]), ), ]), }), ]), ), ]), }), ]); final profile = { 'user': { 'id': 12345, 'username': 'JohnDoe', 'email': 'JOHN@EXAMPLE.COM', 'bio': 'Software developer and coffee enthusiast', 'avatar_url': 'https://example.com/avatar.jpg', }, 'stats': { 'followers': 1234, 'following': 567, 'posts': 89, }, 'recent_posts': [ { 'id': 1, 'content': 'Just shipped a new feature! 🚀', 'likes': 42, 'comments': 5, 'created_at': '2024-01-15T10:00:00Z', 'media': [ { 'type': 'image', 'url': 'https://example.com/post1.jpg', }, ], }, ], }; final result = profileSchema.validate(profile); print('Valid: ${result.isValid}'); print('Username: ${result.data['user']['username']}'); // 'johndoe' ``` ### Comment Thread ```dart final commentSchema = Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'author': Validasi.string([StringRules.minLength(1)]), 'content': Validasi.string([ StringRules.minLength(1), StringRules.maxLength(1000), ]), 'created_at': Validasi.string([StringRules.minLength(1)]), 'likes': Validasi.number([NumberRules.moreThanEqual(0)]), 'replies': Validasi.list>([ Nullable(), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'author': Validasi.string([StringRules.minLength(1)]), 'content': Validasi.string([ StringRules.minLength(1), StringRules.maxLength(500), ]), 'created_at': Validasi.string([StringRules.minLength(1)]), 'likes': Validasi.number([NumberRules.moreThanEqual(0)]), }), ]), ), ]), }), ]); final thread = { 'id': 1, 'author': 'Alice', 'content': 'Great article! Thanks for sharing.', 'created_at': '2024-01-15T10:00:00Z', 'likes': 15, 'replies': [ { 'id': 2, 'author': 'Bob', 'content': 'I agree! Very helpful.', 'created_at': '2024-01-15T10:30:00Z', 'likes': 5, }, ], }; print(commentSchema.validate(thread).isValid); ``` ## API Response Validation ### Paginated API Response ```dart final paginatedResponseSchema = Validasi.map([ MapRules.hasFields({ 'data': Validasi.list>([ IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'title': Validasi.string([StringRules.minLength(1)]), 'description': Validasi.string([Nullable()]), 'created_at': Validasi.string([StringRules.minLength(1)]), }), ]), ), ]), 'pagination': Validasi.map([ MapRules.hasFields({ 'page': Validasi.number([NumberRules.moreThan(0)]), 'per_page': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(100), ]), 'total': Validasi.number([NumberRules.moreThanEqual(0)]), 'total_pages': Validasi.number([NumberRules.moreThanEqual(0)]), }), ]), 'links': Validasi.map([ MapRules.hasFields({ 'first': Validasi.string([Nullable()]), 'last': Validasi.string([Nullable()]), 'prev': Validasi.string([Nullable()]), 'next': Validasi.string([Nullable()]), }), ]), }), ]); final apiResponse = { 'data': [ { 'id': 1, 'title': 'First Item', 'description': 'Description here', 'created_at': '2024-01-15T10:00:00Z', }, ], 'pagination': { 'page': 1, 'per_page': 20, 'total': 100, 'total_pages': 5, }, 'links': { 'first': 'https://api.example.com/items?page=1', 'last': 'https://api.example.com/items?page=5', 'prev': null, 'next': 'https://api.example.com/items?page=2', }, }; print(paginatedResponseSchema.validate(apiResponse).isValid); ``` ### Error Response ```dart final errorResponseSchema = Validasi.map([ MapRules.hasFields({ 'error': Validasi.map([ MapRules.hasFields({ 'code': Validasi.string([StringRules.minLength(1)]), 'message': Validasi.string([StringRules.minLength(1)]), 'details': Validasi.list>([ Nullable(), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'field': Validasi.string([StringRules.minLength(1)]), 'error': Validasi.string([StringRules.minLength(1)]), }), ]), ), ]), }), ]), 'status': Validasi.number([ NumberRules.moreThanEqual(400), NumberRules.lessThan(600), ]), }), ]); final errorResponse = { 'error': { 'code': 'VALIDATION_ERROR', 'message': 'The request contains invalid data', 'details': [ {'field': 'email', 'error': 'Invalid email format'}, {'field': 'age', 'error': 'Must be at least 18'}, ], }, 'status': 422, }; print(errorResponseSchema.validate(errorResponse).isValid); ``` ## Configuration Files ### Application Config ```dart final appConfigSchema = Validasi.map([ MapRules.hasFields({ 'app': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(1)]), 'version': Validasi.string([StringRules.minLength(1)]), 'env': Validasi.string([ StringRules.oneOf(['development', 'staging', 'production']), ]), }), ]), 'server': Validasi.map([ MapRules.hasFields({ 'host': Validasi.string([StringRules.minLength(1)]), 'port': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThan(65536), ]), 'ssl': Validasi.any(), }), ]), 'database': Validasi.map([ MapRules.hasFields({ 'host': Validasi.string([StringRules.minLength(1)]), 'port': Validasi.number([NumberRules.moreThan(0)]), 'name': Validasi.string([StringRules.minLength(1)]), 'user': Validasi.string([StringRules.minLength(1)]), 'password': Validasi.string([StringRules.minLength(1)]), 'pool_size': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(100), ]), }), ]), 'cache': Validasi.map([ MapRules.hasFields({ 'driver': Validasi.string([ StringRules.oneOf(['redis', 'memcached', 'memory']), ]), 'ttl': Validasi.number([NumberRules.moreThan(0)]), }), ]), 'logging': Validasi.map([ MapRules.hasFields({ 'level': Validasi.string([ StringRules.oneOf(['debug', 'info', 'warning', 'error']), ]), 'outputs': Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([ StringRules.oneOf(['console', 'file', 'syslog']), ]), ), ]), }), ]), }), ]); final config = { 'app': { 'name': 'MyApp', 'version': '1.0.0', 'env': 'production', }, 'server': { 'host': '0.0.0.0', 'port': 8080, 'ssl': true, }, 'database': { 'host': 'localhost', 'port': 5432, 'name': 'myapp_db', 'user': 'dbuser', 'password': 'secret', 'pool_size': 20, }, 'cache': { 'driver': 'redis', 'ttl': 3600, }, 'logging': { 'level': 'info', 'outputs': ['console', 'file'], }, }; print(appConfigSchema.validate(config).isValid); ``` ## Forms and Surveys ### Multi-Step Form ```dart final multiStepFormSchema = Validasi.map([ MapRules.hasFields({ 'personal_info': Validasi.map([ MapRules.hasFields({ 'first_name': Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(1), ]), 'last_name': Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(1), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), InlineRule((value) { return value.contains('@') ? null : 'Invalid email'; }), ]), 'phone': Validasi.string([ Nullable(), StringRules.minLength(10), ]), }), ]), 'address': Validasi.map([ MapRules.hasFields({ 'street': Validasi.string([StringRules.minLength(1)]), 'city': Validasi.string([StringRules.minLength(1)]), 'state': Validasi.string([StringRules.minLength(2)]), 'zip': Validasi.string([StringRules.minLength(5)]), 'country': Validasi.string([StringRules.minLength(2)]), }), ]), 'preferences': Validasi.map([ MapRules.hasFields({ 'newsletter': Validasi.any(), 'notifications': Validasi.map([ MapRules.hasFields({ 'email': Validasi.any(), 'sms': Validasi.any(), 'push': Validasi.any(), }), ]), 'interests': Validasi.list([ IterableRules.minLength(1, message: 'Select at least one interest'), IterableRules.forEach( Validasi.string([StringRules.minLength(1)]), ), ]), }), ]), }), ]); final formData = { 'personal_info': { 'first_name': 'John', 'last_name': 'Doe', 'email': 'JOHN@EXAMPLE.COM', 'phone': '555-0123', }, 'address': { 'street': '123 Main St', 'city': 'Springfield', 'state': 'IL', 'zip': '62701', 'country': 'USA', }, 'preferences': { 'newsletter': true, 'notifications': { 'email': true, 'sms': false, 'push': true, }, 'interests': ['technology', 'sports', 'music'], }, }; final result = multiStepFormSchema.validate(formData); print('Valid: ${result.isValid}'); ``` ### Survey Response ```dart final surveySchema = Validasi.map([ MapRules.hasFields({ 'survey_id': Validasi.number([NumberRules.moreThan(0)]), 'respondent': Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([Nullable()]), 'email': Validasi.string([Nullable()]), }), ]), 'responses': Validasi.list>([ IterableRules.minLength(1, message: 'At least one response required'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'question_id': Validasi.number([NumberRules.moreThan(0)]), 'question_type': Validasi.string([ StringRules.oneOf(['text', 'choice', 'rating', 'boolean']), ]), 'answer': Validasi.any([ Required(message: 'Answer is required'), ]), }), ]), ), ]), 'completed_at': Validasi.string([StringRules.minLength(1)]), }), ]); final survey = { 'survey_id': 123, 'respondent': { 'id': 456, 'email': 'user@example.com', }, 'responses': [ { 'question_id': 1, 'question_type': 'text', 'answer': 'I really enjoyed the service', }, { 'question_id': 2, 'question_type': 'rating', 'answer': 5, }, { 'question_id': 3, 'question_type': 'boolean', 'answer': true, }, ], 'completed_at': '2024-01-15T10:00:00Z', }; print(surveySchema.validate(survey).isValid); ``` ## Data Import/Export ### CSV Import Schema ```dart final csvImportSchema = Validasi.map([ MapRules.hasFields({ 'filename': Validasi.string([StringRules.minLength(1)]), 'rows': Validasi.list>([ IterableRules.minLength(1, message: 'CSV must have at least one row'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'row_number': Validasi.number([NumberRules.moreThan(0)]), 'data': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(1, message: 'Name cannot be empty'), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), InlineRule((value) { return value.contains('@') ? null : 'Invalid email'; }), ]), 'age': Validasi.number([ Nullable(), NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(150), ]), }), ]), }), ]), ), ]), 'imported_at': Validasi.string([StringRules.minLength(1)]), }), ]); ``` ## Analytics Data ### Event Tracking ```dart final analyticsSchema = Validasi.map([ MapRules.hasFields({ 'session_id': Validasi.string([StringRules.minLength(1)]), 'user': Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([Nullable()]), 'anonymous_id': Validasi.string([Nullable()]), }), ]), 'events': Validasi.list>([ IterableRules.minLength(1), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(1)]), 'timestamp': Validasi.string([StringRules.minLength(1)]), 'properties': Validasi.map([Nullable()]), 'page': Validasi.map([ Nullable(), MapRules.hasFields({ 'url': Validasi.string([StringRules.minLength(1)]), 'title': Validasi.string([Nullable()]), 'referrer': Validasi.string([Nullable()]), }), ]), }), ]), ), ]), }), ]); final analyticsData = { 'session_id': 'sess_abc123', 'user': { 'id': 12345, 'anonymous_id': null, }, 'events': [ { 'name': 'page_view', 'timestamp': '2024-01-15T10:00:00Z', 'properties': {'source': 'organic'}, 'page': { 'url': 'https://example.com/home', 'title': 'Home Page', 'referrer': 'https://google.com', }, }, { 'name': 'button_click', 'timestamp': '2024-01-15T10:01:00Z', 'properties': {'button_id': 'signup'}, 'page': null, }, ], }; print(analyticsSchema.validate(analyticsData).isValid); ``` ## Next Steps * [String Validation](/examples/string-validation) * [Number Validation](/examples/number-validation) * [List Validation](/examples/list-validation) * [Map Validation](/examples/map-validation) * [Error Handling Guide](/guide/error-handling) --- --- url: /validasi/advanced/custom-rule.md --- # Creating Custom Rules While Validasi provides a comprehensive set of built-in validation rules, you may encounter scenarios that require custom validation logic. This guide explains how to create your own validation rules by extending the `Rule` class. ## Custom Rules vs InlineRule Before diving into custom rules, it's important to understand the difference: * **InlineRule**: A built-in rule that accepts a callback function. Quick and convenient for simple validations, but has limited flexibility (no access to context methods like `stop()` or `setValue()`). * **Custom Rule**: A class extending `Rule`. Provides full control over validation logic, error handling, context manipulation, and can be reused across your application. Use `InlineRule` for quick, one-off validations. Create a custom rule when you need: * Reusable validation logic * Complex validation with multiple conditions * Context manipulation (transforming values, stopping validation) * Custom error details and structured error messages ## Basic Structure Every custom rule extends `Rule` and implements the `apply` method: ```dart import 'package:validasi/src/engine/context.dart'; import 'package:validasi/src/engine/error.dart'; import 'package:validasi/src/engine/rule.dart'; class MyCustomRule extends Rule { const MyCustomRule({super.message}); @override void apply(ValidationContext context) { // Your validation logic here if (context.requireValue.isEmpty) { context.addError( ValidationError( rule: 'MyCustomRule', message: message ?? 'Value cannot be empty', ), ); } } } ``` ## The `runOnNull` Property The `runOnNull` property controls whether your rule should execute when the value is `null`. By default, it's `false`. ```dart class EmailRule extends Rule { const EmailRule({super.message}); @override bool get runOnNull => false; // Skip validation if value is null @override void apply(ValidationContext context) { final email = context.requireValue; if (!email.contains('@')) { context.addError( ValidationError( rule: 'Email', message: message ?? 'Invalid email format', ), ); } } } ``` ### When to Use `runOnNull` Set `runOnNull = true` when your rule needs to: * Validate null values (like `Required` rule) * Transform null values (like `Transform` rule) * Provide default values for null inputs ```dart class DefaultValue extends Rule { const DefaultValue(this.defaultValue, {super.message}); final T defaultValue; @override bool get runOnNull => true; // Must run on null to set default @override void apply(ValidationContext context) { if (context.value == null) { context.setValue(defaultValue); } } } ``` ## Message Overriding The `message` parameter allows users to customize error messages when using your rule: ```dart class MinAge extends Rule { const MinAge(this.minAge, {super.message}); final int minAge; @override void apply(ValidationContext context) { if (context.requireValue < minAge) { context.addError( ValidationError( rule: 'MinAge', // Use custom message if provided, otherwise use default message: message ?? 'Age must be at least $minAge', details: {'minAge': minAge.toString()}, ), ); } } } // Usage with custom message final schema = ValidasiEngine( rules: [ MinAge(18, message: 'You must be 18 or older to register'), ], ); ``` ### Best Practices for Messages 1. **Always provide a default message** using the `??` operator 2. **Include relevant values** in the default message for clarity 3. **Add error details** for programmatic error handling 4. **Keep messages user-friendly** and actionable ```dart class RangeRule extends Rule { const RangeRule(this.min, this.max, {super.message}); final int min; final int max; @override void apply(ValidationContext context) { final value = context.requireValue; if (value < min || value > max) { context.addError( ValidationError( rule: 'Range', message: message ?? 'Value must be between $min and $max', details: { 'min': min.toString(), 'max': max.toString(), 'value': value.toString(), }, ), ); } } } ``` ## Working with ValidationContext The `ValidationContext` provides several methods and properties for interacting with the validation process: ### Accessing the Value ```dart // Get the current value (nullable) T? value = context.value; // Get the value (throws if null - use when runOnNull = false) T value = context.requireValue; ``` ### Adding Errors ```dart context.addError( ValidationError( rule: 'RuleName', message: 'Error message', details: {'key': 'value'}, // Optional metadata ), ); ``` ### Transforming Values Custom rules can modify the value during validation: ```dart class TrimString extends Rule { const TrimString({super.message}); @override void apply(ValidationContext context) { final trimmed = context.requireValue.trim(); context.setValue(trimmed); } } ``` ### Stopping Validation Stop the validation chain to prevent subsequent rules from running: ```dart class StopIfEmpty extends Rule { const StopIfEmpty({super.message}); @override void apply(ValidationContext context) { if (context.requireValue.isEmpty) { context.addError( ValidationError( rule: 'StopIfEmpty', message: message ?? 'Value is required', ), ); context.stop(); // Prevents further rules from executing } } } ``` ## Complete Example: URL Validation Rule Here's a comprehensive example demonstrating all concepts: ```dart import 'package:validasi/src/engine/context.dart'; import 'package:validasi/src/engine/error.dart'; import 'package:validasi/src/engine/rule.dart'; class UrlRule extends Rule { const UrlRule({ this.schemes = const ['http', 'https'], this.requireTld = true, super.message, }); final List schemes; final bool requireTld; @override bool get runOnNull => false; // Don't validate null values @override void apply(ValidationContext context) { final url = context.requireValue; // Check if URL has valid scheme bool hasValidScheme = false; for (final scheme in schemes) { if (url.startsWith('$scheme://')) { hasValidScheme = true; break; } } if (!hasValidScheme) { context.addError( ValidationError( rule: 'Url', message: message ?? 'URL must start with ${schemes.join(" or ")}://', details: { 'allowedSchemes': schemes.join(', '), 'value': url, }, ), ); return; } // Check for TLD if required if (requireTld && !url.contains('.')) { context.addError( ValidationError( rule: 'Url', message: message ?? 'URL must contain a valid domain with TLD', details: {'requireTld': requireTld.toString()}, ), ); } } } // Usage final schema = ValidasiEngine( rules: [ UrlRule( schemes: ['http', 'https', 'ftp'], requireTld: true, message: 'Please enter a valid URL', ), ], ); final result = schema.validate('https://example.com'); ``` ## Advanced Patterns ### Parameterized Rules Create flexible rules that accept configuration: ```dart class Contains extends Rule { const Contains(this.substring, {this.caseSensitive = true, super.message}); final String substring; final bool caseSensitive; @override void apply(ValidationContext context) { final value = context.requireValue; final contains = caseSensitive ? value.contains(substring) : value.toLowerCase().contains(substring.toLowerCase()); if (!contains) { context.addError( ValidationError( rule: 'Contains', message: message ?? 'Value must contain "$substring"', details: { 'substring': substring, 'caseSensitive': caseSensitive.toString(), }, ), ); } } } ``` ### Generic Type Rules Create rules that work with multiple types: ```dart class NotEqual extends Rule { const NotEqual(this.forbidden, {super.message}); final T forbidden; @override void apply(ValidationContext context) { if (context.requireValue == forbidden) { context.addError( ValidationError( rule: 'NotEqual', message: message ?? 'Value must not equal $forbidden', details: {'forbidden': forbidden.toString()}, ), ); } } } // Works with any type ValidasiEngine(rules: [NotEqual(0)]); ValidasiEngine(rules: [NotEqual('')]); ``` ### Composite Validation Combine multiple checks in one rule: ```dart class PasswordStrength extends Rule { const PasswordStrength({super.message}); @override void apply(ValidationContext context) { final password = context.requireValue; final errors = []; if (password.length < 8) { errors.add('at least 8 characters'); } if (!password.contains(RegExp(r'[A-Z]'))) { errors.add('one uppercase letter'); } if (!password.contains(RegExp(r'[0-9]'))) { errors.add('one number'); } if (!password.contains(RegExp(r'[!@#$%^&*]'))) { errors.add('one special character'); } if (errors.isNotEmpty) { context.addError( ValidationError( rule: 'PasswordStrength', message: message ?? 'Password must contain ${errors.join(", ")}', details: {'requirements': errors}, ), ); } } } ``` ## Testing Custom Rules Always test your custom rules thoroughly: ```dart import 'package:test/test.dart'; import 'package:validasi/validasi.dart'; void main() { group('UrlRule', () { test('accepts valid HTTPS URL', () { final schema = ValidasiEngine(rules: [UrlRule()]); final result = schema.validate('https://example.com'); expect(result.isValid, true); }); test('rejects URL without scheme', () { final schema = ValidasiEngine(rules: [UrlRule()]); final result = schema.validate('example.com'); expect(result.isValid, false); expect(result.errors.first.rule, 'Url'); }); test('accepts custom message', () { final schema = ValidasiEngine( rules: [UrlRule(message: 'Custom error')], ); final result = schema.validate('invalid'); expect(result.errors.first.message, 'Custom error'); }); }); } ``` ## Summary Creating custom rules in Validasi is straightforward: 1. Extend `Rule` with your specific type 2. Override `runOnNull` if you need to handle null values 3. Implement `apply(ValidationContext context)` with your validation logic 4. Use `context.addError()` to report validation failures 5. Support message customization via the `message` parameter 6. Use context methods (`setValue`, `stop`) for advanced scenarios 7. Add error details for better debugging and programmatic handling Custom rules give you full control while maintaining the composability and type safety that makes Validasi powerful. --- --- url: /validasi/v0/guide/custom-rule.md --- # Custom Rule Validasi provides a way to create a custom rule by using the `custom` and `customFor` method. The custom rule will then be called when the built-in rules already performed. For details, see [Execution Flow](/v0/guide/execution-order). ## `custom` Method The `custom` method will receive a function that will be called when the validation is performed. ```dart import 'package:validasi/validasi.dart'; Validasi.string().custom((value, fail) {}); ``` ## `customFor` Method The `customFor` method will receive a class instance that implements the `CustomRule` interface. This method is useful when you want to create a custom rule that can be reused in multiple schemas. ::: code-group ```dart [main.dart] import 'package:validasi/validasi.dart'; import 'package:app/rules/my_rule.dart'; Validasi.string().customFor(MyRule()); // [!code focus] ``` ```dart [my_rule.dart] import 'dart:async'; import 'package:validasi/validasi.dart'; class MyRule extends CustomRule { // [!code focus:6] @override FutureOr handle(value, fail) { return true; } } ``` ::: ## Asyncronous Custom Rule You can also run an async function inside the custom rule. ```dart import 'package:validasi/validasi.dart'; import 'package:app/data.dart'; void main() { final schema = Validasi.string().custom((value, fail) async { if (await Data.user().contains(value)) { return true; } return fail('Ups, not hello'); }); final result = schema.tryParseAsync('world'); } ``` Keep in mind when running async rule, you need to use the async variants instead (e.g. `tryParseAsync`, `parseAsync`). ## The Method Signature Both the `custom` and `handle` method share similar signature. The `value` parameter is the value that will be validated by your custom rule. The `fail` parameter is a function that you can call when the validation failed. Both methods expects `FutureOr` as the return type. You can return `true` to indicate that the validation passed or `false` to indicate that the validation failed. You can also return a `Future` if you want to run an async since `FutureOr` is a union type of `Future` and `T`. The `fail` parameter is a function that will receive a message that will be used as the error message. The `fail` function will return `false` to indicate that the validation failed. Alternatively, you can just return `false` this will fallback to default error message: `field is not valid`. ::: info When running async rule in a non-async parse variants (e.g. `tryParse`, `parse`), Validasi will throw an exception. ::: --- --- url: /validasi/v0/types/date.md --- # Date The `Date` schema is used to validate the input value as a `DateTime`. This schema contains some useful built-in validators to validate the input value. The following code shows how to create a `Date` schema: ::: code-group ```dart [Using Validasi] Validasi.date(); ``` ```dart [Using Direct Class] DateValidator(); ``` ::: Below are the available methods for the `Date` schema: \[\[toc]] ## DateUnit Enum The `DateUnit` enum is used to specify the unit of the date difference. ```dart enum DateUnit { day, month, year, } ``` ## before ```dart Validasi.date().before(DateTime target, {DateUnit unit = DateUnit.day, int difference = 1, String? message}); ``` The `before` method is used to validate that the input date is before the specified target date. This method will return an error message if the input date is not before the target date. The `unit` parameter is used to specify the unit of the date difference. The default value is `DateUnit.day`. The `difference` parameter is used to specify the number of units to compare. The default value is `1`. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.date().before(DateTime(2023, 1, 1)); final result = schema.tryParse(DateTime(2023, 1, 2)); print(result.errors.first.message); // 'field must be before 2023-01-01' } ``` ## after ```dart Validasi.date().after(DateTime target, {DateUnit unit = DateUnit.day, int difference = 1, String? message}); ``` The `after` method is used to validate that the input date is after the specified target date. This method will return an error message if the input date is not after the target date. The `unit` parameter is used to specify the unit of the date difference. The default value is `DateUnit.day`. The `difference` parameter is used to specify the number of units to compare. The default value is `1`. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.date().after(DateTime(2023, 1, 1)); final result = schema.tryParse(DateTime(2022, 12, 31)); print(result.errors.first.message); // 'field must be after 2023-01-01' } ``` ## beforeSame ```dart Validasi.date().beforeSame(DateTime target, {DateUnit unit = DateUnit.day, String? message}); ``` The `beforeSame` method is used to validate that the input date is before or the same as the specified target date. This method will return an error message if the input date is not before or the same as the target date. The `unit` parameter is used to specify the unit of the date difference. The default value is `DateUnit.day`. ::: tip This is similar to the `before` method, with `difference` set to `0`. ::: ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.date().beforeSame(DateTime(2023, 1, 1)); final result = schema.tryParse(DateTime(2023, 1, 2)); print(result.errors.first.message); // 'field must be before or equal 2023-01-01' } ``` ## afterSame ```dart Validasi.date().afterSame(DateTime target, {DateUnit unit = DateUnit.day, String? message}); ``` The `afterSame` method is used to validate that the input date is after or the same as the specified target date. This method will return an error message if the input date is not after or the same as the target date. The `unit` parameter is used to specify the unit of the date difference. The default value is `DateUnit.day`. ::: tip This is similar to the `after` method, with `difference` set to `0`. ::: ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.date().afterSame(DateTime(2023, 1, 1)); final result = schema.tryParse(DateTime(2022, 12, 31)); print(result.errors.first.message); // 'field must be after or equal 2023-01-01' } ``` --- --- url: /validasi/guide/error-handling.md --- # Error Handling Proper error handling is crucial for providing good user feedback and debugging validation issues. This guide covers how to work with validation errors in Validasi. ## Understanding ValidasiResult Every validation returns a `ValidasiResult` object: ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final schema = Validasi.string([ StringRules.minLength(3), ]); final result = schema.validate('Hi'); ``` The `ValidasiResult` contains: * **`isValid: bool`** - Whether validation succeeded * **`data: T`** - The validated (possibly transformed) data * **`errors: List`** - List of validation errors ## Checking Validation Status ### Basic Check ```dart final result = schema.validate(data); if (result.isValid) { print('✓ Valid data: ${result.data}'); } else { print('✗ Validation failed'); for (var error in result.errors) { print(' - ${error.message}'); } } ``` ### Using Pattern Matching ```dart final result = schema.validate(data); switch (result.isValid) { case true: // Handle success processData(result.data); break; case false: // Handle errors showErrors(result.errors); break; } ``` ## Working with ValidasiError Each `ValidasiError` contains: * **`message: String`** - The error message * **`path: List?`** - Path to the failed field (for nested structures) ### Simple Error Messages ```dart final nameSchema = Validasi.string([ StringRules.minLength(3, message: 'Name is too short'), StringRules.maxLength(50, message: 'Name is too long'), ]); final result = nameSchema.validate('Hi'); if (!result.isValid) { for (var error in result.errors) { print(error.message); // "Name is too short" } } ``` ### Error Paths for Nested Data Error paths help identify exactly where validation failed in nested structures: ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'profile': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1, message: 'Name is required'), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(0, message: 'Age must be positive'), ]), }), ]), }), ]); final result = userSchema.validate({ 'profile': { 'name': '', 'age': -5, }, }); for (var error in result.errors) { final path = error.path?.join('.') ?? 'root'; print('[$path] ${error.message}'); } // Output: // [profile.name] Name is required // [profile.age] Age must be positive ``` ## Custom Error Messages ### Built-in Rules Most rules accept a custom message: ```dart final passwordSchema = Validasi.string([ StringRules.minLength(8, message: 'Password must be at least 8 characters'), StringRules.maxLength(128, message: 'Password is too long (max 128 characters)'), ]); ``` ### InlineRule Create custom errors with `InlineRule`: ```dart final schema = Validasi.string([ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain at least one uppercase letter'; } if (!value.contains(RegExp(r'[0-9]'))) { return 'Must contain at least one number'; } return null; // null means valid }), ]); ``` ### Multiple Error Messages InlineRule returns the first error encountered: ```dart final emailSchema = Validasi.string([ InlineRule((value) { if (!value.contains('@')) { return 'Email must contain @'; } if (!value.contains('.')) { return 'Email must contain a domain'; } if (value.length < 5) { return 'Email is too short'; } return null; }), ]); ``` ## Collecting All Errors By default, validation returns all errors: ```dart final schema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1), ]), 'email': Validasi.string([ StringRules.minLength(5), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(0), ]), }), ]); final result = schema.validate({ 'name': '', // Error 'email': 'ab', // Error 'age': -5, // Error }); print('Total errors: ${result.errors.length}'); // 3 ``` ## Formatting Error Messages ### Simple List ```dart void displayErrors(ValidasiResult result) { if (!result.isValid) { print('Validation failed:'); for (var error in result.errors) { print('• ${error.message}'); } } } ``` ### With Paths ```dart void displayErrorsWithPath(ValidasiResult result) { if (!result.isValid) { print('Validation errors:'); for (var error in result.errors) { final location = error.path?.join('.') ?? 'root'; print('[$location]: ${error.message}'); } } } ``` ### For UI Display ```dart Map> groupErrorsByField(ValidasiResult result) { final grouped = >{}; for (var error in result.errors) { final field = error.path?.last ?? 'root'; grouped.putIfAbsent(field, () => []).add(error.message); } return grouped; } // Usage final errors = groupErrorsByField(result); errors.forEach((field, messages) { print('$field:'); for (var message in messages) { print(' - $message'); } }); ``` ### JSON Format ```dart List> errorsToJson(ValidasiResult result) { return result.errors.map((error) => { 'field': error.path?.join('.'), 'message': error.message, }).toList(); } // Usage final jsonErrors = errorsToJson(result); print(jsonEncode(jsonErrors)); // [{"field":"profile.name","message":"Name is required"}] ``` ## Error Handling Patterns ### Early Return Pattern ```dart Future saveUser(Map userData) async { final result = userSchema.validate(userData); if (!result.isValid) { throw ValidationException(result.errors); } await database.save(result.data); } ``` ### Result Pattern ```dart class SaveResult { final bool success; final String? error; final User? user; SaveResult.success(this.user) : success = true, error = null; SaveResult.failure(this.error) : success = false, user = null; } Future saveUser(Map userData) async { final result = userSchema.validate(userData); if (!result.isValid) { final errorMsg = result.errors .map((e) => e.message) .join(', '); return SaveResult.failure(errorMsg); } final user = await database.save(result.data); return SaveResult.success(user); } ``` ### Exception Wrapper ```dart class ValidationException implements Exception { final List errors; ValidationException(this.errors); @override String toString() { return 'ValidationException: ${errors.map((e) => e.message).join(', ')}'; } } void processData(dynamic data) { final result = schema.validate(data); if (!result.isValid) { throw ValidationException(result.errors); } // Continue with valid data print('Processing: ${result.data}'); } ``` ## Flutter Integration ### Form Validation ```dart class RegistrationForm extends StatefulWidget { @override _RegistrationFormState createState() => _RegistrationFormState(); } class _RegistrationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _emailSchema = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), ]); final _passwordSchema = Validasi.string([ StringRules.minLength(8), ]); String? _validateEmail(String? value) { final result = _emailSchema.validate(value); return result.isValid ? null : result.errors.first.message; } String? _validatePassword(String? value) { final result = _passwordSchema.validate(value); return result.isValid ? null : result.errors.first.message; } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), validator: _validateEmail, ), TextFormField( controller: _passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true, validator: _validatePassword, ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Form is valid } }, child: Text('Submit'), ), ], ), ); } } ``` ### Showing Multiple Errors ```dart Widget buildErrorList(List errors) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: errors.map((error) { return Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Icon(Icons.error, color: Colors.red, size: 16), SizedBox(width: 8), Expanded( child: Text( error.message, style: TextStyle(color: Colors.red), ), ), ], ), ); }).toList(), ); } ``` ## Best Practices ### 1. Provide Clear Messages ```dart // Good ✓ StringRules.minLength(8, message: 'Password must be at least 8 characters') // Less helpful ✗ StringRules.minLength(8, message: 'Invalid') ``` ### 2. Be Specific ```dart // Good ✓ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain at least one uppercase letter'; } return null; }) // Vague ✗ InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Invalid password'; } return null; }) ``` ### 3. Handle All Error Cases ```dart final result = schema.validate(data); if (result.isValid) { // Success path processData(result.data); } else { // Error path - don't ignore! logErrors(result.errors); notifyUser(result.errors); } ``` ### 4. Group Related Errors ```dart Map> groupBySection(List errors) { final groups = >{}; for (var error in errors) { final section = error.path?.first ?? 'general'; groups.putIfAbsent(section, () => []).add(error.message); } return groups; } ``` ## Next Steps * [Examples](/examples/string-validation) - See practical examples * [Advanced](/advanced/custom-rule) - Create custom validation rules * [API Reference](https://pub.dev/documentation/validasi/latest) - Full API documentation --- --- url: /validasi/v0/types/generic.md --- # Generic The Generic Validation is a new feature provided in Validasi starting from version 0.0.4. This new validation allow you to validate any type of data for instance checking, transforming, and etc. The `GenericValidator` expect `T` as the type to validate against. Available rules for `GenericValidator`: \[\[toc]] ## `equalTo` ```dart Validasi.generic(transformer).equalTo(T value, {String? message}); ``` The `equalTo` method is used to validate that the value is equal to the specified value. This method will return an error if the value is not equal to the specified value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.generic().equalTo('a'); final result = schema.tryParse('b'); print(result.errors.first.message); // 'field must be equal to a' } ``` ::: info The equality check uses `==` operator to compare the value. Therefore, it is recommended for you to override the `==` operator in your custom class to ensure the equality check works as expected. ::: ## `notEqualTo` ```dart Validasi.generic(transformer).notEqualTo(T value, {String? message}); ``` The `notEqualTo` method is used to validate that the value is not equal to the specified value. This method will return an error message if the value is equal to the specified value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.generic().notEqualTo('a'); final result = schema.tryParse('a'); print(result.errors.first.message); // 'field must not be equal to a' } ``` ::: info Similar to `equalTo`, the equality check uses `!=` operator to compare the value. Therefore, it is recommended for you to override the `==` operator in your custom class to ensure the equality check works as expected. ::: ## `oneOf` ```dart Validasi.generic(transformer).oneOf(List values, {String? message}); ``` The `oneOf` method is used to validate that the value is one of the specified values. This method will return an error message if the value is not in the specified list. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.generic().oneOf(['a', 'b']); final result = schema.tryParse('c'); print(result.errors.first.message); // 'field must be one of a, b' } ``` ::: info Similar to `equalTo` but with multiple values. It will return an error message if the value is not in the specified list. ::: ## `notOneOf` ```dart Validasi.generic(transformer).notOneOf(List values, {String? message}); ``` The `notOneOf` method is used to validate that the value is not one of the specified values. This method will return an error message if the value is in the specified list. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.generic().notOneOf(['a', 'b']); final result = schema.tryParse('a'); print(result.errors.first.message); // 'field must not be one of a, b' } ``` ::: info Just like oneOf, but the opposite. It will return an error message if the value is in the specified list. ::: --- --- url: /validasi/guide/getting-started.md --- # Getting Started ::: warning 📢 v1.0.0-dev Documentation You are viewing the documentation for **Validasi v1.0.0-dev** (development version). This version includes significant API changes and new features. Looking for the stable version? [View v0 Documentation →](/v0/) ::: Welcome to Validasi! This guide will help you get started with the most flexible and type-safe validation library for Dart and Flutter. ## What is Validasi? Validasi is a powerful validation library that brings type safety, composability, and elegance to data validation in Dart and Flutter applications. Whether you're validating user input, API responses, or configuration files, Validasi makes it simple and maintainable. ## Key Features ::: tip Type Safety First Validasi is built with Dart's type system in mind, providing full type safety throughout your validation logic. No more runtime surprises! ::: ::: info Composable & Reusable Create complex validation schemas from simple, reusable rules. Mix and match rules to build exactly what you need. ::: ::: warning Performance Optimized Built-in caching system ensures your validations run fast, even when validating thousands of objects. ::: ## Why Choose Validasi? ### 🎯 Intuitive API ```dart final schema = Validasi.string([ StringRules.minLength(3), StringRules.maxLength(20), ]); final result = schema.validate('Hello'); ``` Simple, readable, and self-documenting code. ### 🔧 Comprehensive Rules Built-in rules for every common validation scenario: * String validation (length, patterns, formats) * Number validation (ranges, comparisons) * Collection validation (lists, maps) * Custom validation (inline rules, transformations) ### 🌲 Nested Validation Validate complex, nested data structures with ease: ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'profile': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(2)]), 'age': Validasi.number([NumberRules.moreThan(0)]), }), ]), }), ]); ``` ### 🎨 Flexible Transformations Transform data during validation: ```dart final schema = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(3), ]); ``` ## Next Steps Ready to dive in? Check out: * [Installation Guide](/guide/installation) - Add Validasi to your project * [Quick Start](/guide/quick-start) - Build your first validation schema * [String Examples](/examples/string-validation) - See practical examples ## Community & Support * **GitHub**: [albetnov/validasi](https://github.com/albetnov/validasi) * **Issues**: [Report bugs or request features](https://github.com/albetnov/validasi/issues) * **Pub.dev**: [Package documentation](https://pub.dev/packages/validasi) *** ## LLM Support This documentation has support for LLMS, you can click the button at the bottom right corner to copy or download the current page as markdown file for your LLM processing. Or you can visit [`llms-full.txt`](/llms-full.txt) or [`llms.txt`](/llms.txt) files. --- --- url: /validasi/v0/guide/helpers.md --- # Helpers Validasi provides a few helper classes to help you for Flutter Development. ## FieldValidator The `FieldValidator` is a class that can be used to validate a single schema. This class is useful since you can get first error message from the schema directly. ```dart import 'package:validasi/validasi.dart'; final schema = Validasi.string().minLength(3); TextFormField( validator: FieldValidator(schema).validate, ); ``` ::: info For asyncronous validation, you can use `validateAsync` method instead. ::: ## GroupValidator The `GroupValidator` is a wrapper around the `FieldValidator` that can be used to nicely grouped some schema together. This class is useful when you have multiple schema and you want to place them in one place. ```dart import 'package:validasi/validasi.dart'; final schema = GroupValidator({ 'name': Validasi.string().minLength(3), 'email': Validasi.string().email(), }); TextFormField( validator: schema.using('name').validate, ); TextFormField( validator: (value) => schema.using('email').validate, ); ``` ::: info For asyncronous validation, you can use `validateAsync` method instead. ::: ### With mapped values You can also use `GroupValidator` with mapped values. This is useful when you want to validate a group of values at once and you want to use the mapped values as the key. ```dart import 'package:validasi/validasi.dart'; final schema = GroupValidator({ 'name': Validasi.string().minLength(3), 'email': Validasi.string().email(), }); final values = { 'name': 'John Doe', 'email': 'john@example.com', }; schema.validateMap(values); // validateMap and validateMapAsync ``` ::: warning Mapped Based Validation is `strict`. Which means your value should match the key in the schema. If you have a schema with `name` and `email`, but your value only has `name`, it will throw an error. ::: ## Customizing The Path Error Message You can customize the path error message by passing the `path` parameter to the `FieldValidator` or `GroupValidator`: ```dart import 'package:validasi/validasi.dart'; FieldValidator(Validasi.string(), path: 'Custom Path').validate('Invalid Value'); GroupValidator({ 'name': Validasi.string().minLength(3), 'email': Validasi.string().email(), }).on('name', path: 'Custom Path').validate('Invalid Value'); ``` --- --- url: /validasi/guide/installation.md --- # Installation Get Validasi up and running in your Dart or Flutter project in just a few steps. ## Requirements * Dart SDK: `>=3.0.0 <4.0.0` * Flutter: Any version supporting Dart 3.0+ ## Add Dependency ### Using Command Line For Dart projects: ```bash dart pub add validasi ``` For Flutter projects: ```bash flutter pub add validasi ``` ### Manual Installation Add Validasi to your `pubspec.yaml`: ```yaml dependencies: validasi: ^1.0.0-dev.0 ``` Then run: ```bash dart pub get # For Dart projects # or flutter pub get # For Flutter projects ``` ## Import the Library In your Dart files, import Validasi: ```dart // Core validation engine import 'package:validasi/validasi.dart'; // Built-in validation rules import 'package:validasi/rules.dart'; // Optional: Transformations import 'package:validasi/transformer.dart'; ``` ## Verify Installation Create a simple test to verify everything works: ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; void main() { final schema = Validasi.string([ StringRules.minLength(3), ]); final result = schema.validate('Hello'); print('Valid: ${result.isValid}'); // Output: Valid: true print('Data: ${result.data}'); // Output: Data: Hello } ``` If you see the expected output, you're all set! 🎉 ## IDE Setup ### VS Code Install the Dart extension for better IntelliSense and code completion: ``` ext install Dart-Code.dart-code ``` ### IntelliJ IDEA / Android Studio The Dart and Flutter plugins come pre-installed with IntelliJ IDEA and Android Studio for Flutter development. ## Next Steps Now that Validasi is installed, let's build your first validation schema: ## Troubleshooting ### Version Conflicts If you encounter version conflicts, try: ```bash dart pub upgrade validasi # or flutter pub upgrade validasi ``` ### Import Errors Make sure you've run `pub get` after adding the dependency: ```bash dart pub get # or flutter pub get ``` ### Need Help? * Check the [GitHub Issues](https://github.com/albetnov/validasi/issues) * Review the [API Documentation](https://pub.dev/documentation/validasi/latest) * [String Examples](/examples/string-validation) - See practical examples --- --- url: /validasi/rules/iterable.md --- # Iterable Rules List and iterable validation rules for collections. ## IterableRules.minLength() Validates that an iterable (list, set, etc.) has a minimum number of elements. ### Syntax ```dart IterableRules.minLength(int length, {String? message}) ``` ### Parameters * **`T`** - The type of elements in the iterable * **`length`** (int, required) - The minimum number of elements required * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Minimum length is {length} items" ``` ### Example ```dart final tagsSchema = Validasi.list([ IterableRules.minLength(1, message: 'At least one tag is required'), ]); print(tagsSchema.validate([]).isValid); // false print(tagsSchema.validate(['dart']).isValid); // true ``` ## IterableRules.forEach() Validates each element in an iterable using a specified schema. ### Syntax ```dart IterableRules.forEach(ValidasiEngine validator) ``` ### Parameters * **`T`** - The type of elements in the iterable * **`validator`** (`ValidasiEngine`, required) - The validation schema to apply to each element ### Example ```dart final emailsSchema = Validasi.list([ IterableRules.forEach( Validasi.string([ Transform((value) => value?.trim().toLowerCase()), InlineRule((value) { return value.contains('@') ? null : 'Invalid email'; }), ]) ), ]); final result = emailsSchema.validate(['JOHN@EXAMPLE.COM', 'JANE@EXAMPLE.COM']); print(result.isValid); // true print(result.data); // ['john@example.com', 'jane@example.com'] ``` ## See Also * [List Validation Examples](/examples/list-validation) --- --- url: /validasi/examples/list-validation.md --- # List Validation Examples Learn how to validate lists and iterables with Validasi's list validation rules. ## Basic List Validation ### Simple List ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final tagsSchema = Validasi.list([ IterableRules.minLength(1, message: 'At least one tag required'), ]); // Valid print(tagsSchema.validate(['dart', 'flutter']).isValid); // true // Invalid print(tagsSchema.validate([]).isValid); // false ``` ### List with Element Validation ```dart final namesSchema = Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([ StringRules.minLength(2), ]) ), ]); // Valid print(namesSchema.validate(['John', 'Jane', 'Bob']).isValid); // true // Invalid - empty string print(namesSchema.validate(['John', '', 'Bob']).isValid); // false ``` ## Length Validation ### Minimum Length ```dart final minSchema = Validasi.list([ IterableRules.minLength(3, message: 'List must have at least 3 items'), ]); print(minSchema.validate([1, 2, 3]).isValid); // true print(minSchema.validate([1, 2]).isValid); // false ``` ### Combined Length Rules ```dart final rangeSchema = Validasi.list([ IterableRules.minLength(2), IterableRules.forEach( Validasi.string([ StringRules.minLength(1), ]) ), InlineRule>((list) { if (list.length > 10) { return 'Too many items (max 10)'; } return null; }), ]); // Valid print(rangeSchema.validate(['a', 'b', 'c']).isValid); // true // Invalid - too few print(rangeSchema.validate(['a']).isValid); // false // Invalid - too many print(rangeSchema.validate(List.generate(11, (i) => 'item$i')).isValid); // false ``` ## ForEach Validation ### String List ```dart final emailsSchema = Validasi.list([ IterableRules.forEach( Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), InlineRule((value) { return value.contains('@') ? null : 'Invalid email'; }), ]) ), ]); final result = emailsSchema.validate([ 'JOHN@EXAMPLE.COM', 'JANE@EXAMPLE.COM', ]); print(result.isValid); // true print(result.data); // ['john@example.com', 'jane@example.com'] ``` ### Number List ```dart final scoresSchema = Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(100), ]) ), ]); // Valid print(scoresSchema.validate([85, 92, 78]).isValid); // true // Invalid - out of range print(scoresSchema.validate([85, 105, 78]).isValid); // false ``` ## Nested Lists ### List of Lists ```dart final matrixSchema = Validasi.list>([ IterableRules.minLength(1), IterableRules.forEach( Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.number([ NumberRules.moreThanEqual(0), ]) ), ]) ), ]); // Valid - 2D matrix final matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ]; print(matrixSchema.validate(matrix).isValid); // true ``` ### List of Maps ```dart final usersSchema = Validasi.list>([ IterableRules.minLength(1), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([ NumberRules.moreThan(0), ]), 'name': Validasi.string([ StringRules.minLength(1), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), ]), }), ]) ), ]); final users = [ {'id': 1, 'name': 'John', 'email': 'JOHN@EXAMPLE.COM'}, {'id': 2, 'name': 'Jane', 'email': 'JANE@EXAMPLE.COM'}, ]; final result = usersSchema.validate(users); print('Valid: ${result.isValid}'); print('First email: ${result.data[0]['email']}'); // john@example.com ``` ## Nullable Lists ### Optional List ```dart final optionalTagsSchema = Validasi.list([ Nullable(), IterableRules.minLength(1), IterableRules.forEach( Validasi.string([StringRules.minLength(2)]) ), ]); // Valid print(optionalTagsSchema.validate(null).isValid); // true print(optionalTagsSchema.validate(['dart', 'flutter']).isValid); // true // Invalid - empty list print(optionalTagsSchema.validate([]).isValid); // false ``` ### List with Nullable Elements ```dart final mixedSchema = Validasi.list([ IterableRules.forEach( Validasi.string([ Nullable(), StringRules.minLength(2), ]) ), ]); // Valid - contains nulls print(mixedSchema.validate(['hello', null, 'world']).isValid); // true ``` ## List Transformations ### Filter Empty Strings ```dart final cleanListSchema = Validasi.list([ Transform((list) { return list?.where((item) => item.isNotEmpty).toList(); }), IterableRules.minLength(1), ]); final result = cleanListSchema.validate(['a', '', 'b', '', 'c']); print(result.data); // ['a', 'b', 'c'] ``` ### Trim All Strings ```dart final trimmedSchema = Validasi.list([ IterableRules.forEach( Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(1), ]) ), ]); final result = trimmedSchema.validate([' hello ', ' world ']); print(result.data); // ['hello', 'world'] ``` ### Sort List ```dart final sortedSchema = Validasi.list([ Transform((list) { if (list == null) return null; final sorted = List.from(list); sorted.sort(); return sorted; }), ]); final result = sortedSchema.validate([3, 1, 4, 1, 5, 9]); print(result.data); // [1, 1, 3, 4, 5, 9] ``` ### Remove Duplicates ```dart final uniqueSchema = Validasi.list([ Transform((list) { return list?.toSet().toList(); }), IterableRules.minLength(1), ]); final result = uniqueSchema.validate(['a', 'b', 'a', 'c', 'b']); print(result.data); // ['a', 'b', 'c'] ``` ## Real-World Examples ### Todo List ```dart final todoListSchema = Validasi.list>([ IterableRules.minLength(1, message: 'Add at least one task'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([ NumberRules.moreThan(0), ]), 'title': Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(1, message: 'Task title required'), StringRules.maxLength(200), ]), 'completed': Validasi.any(), 'priority': Validasi.string([ Nullable(), StringRules.oneOf(['low', 'medium', 'high']), ]), }), ]) ), ]); final todos = [ { 'id': 1, 'title': ' Complete documentation ', 'completed': false, 'priority': 'high', }, { 'id': 2, 'title': 'Review pull requests', 'completed': true, 'priority': 'medium', }, ]; final result = todoListSchema.validate(todos); print('Valid: ${result.isValid}'); print('First task: ${result.data[0]['title']}'); // 'Complete documentation' ``` ### File Upload List ```dart final filesSchema = Validasi.list>([ IterableRules.minLength(1, message: 'Select at least one file'), InlineRule>>((list) { if (list.length > 5) { return 'Maximum 5 files allowed'; } return null; }), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1), ]), 'size': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(10 * 1024 * 1024), // 10MB ]), 'type': Validasi.string([ StringRules.oneOf([ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', ]), ]), }), ]) ), ]); final files = [ { 'name': 'photo.jpg', 'size': 2048576, // 2MB 'type': 'image/jpeg', }, { 'name': 'document.pdf', 'size': 1024000, // 1MB 'type': 'application/pdf', }, ]; print(filesSchema.validate(files).isValid); // true ``` ### Shopping Cart ```dart final cartSchema = Validasi.list>([ IterableRules.minLength(1, message: 'Cart is empty'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'product_id': Validasi.number([ NumberRules.moreThan(0), ]), 'name': Validasi.string([ StringRules.minLength(1), ]), 'quantity': Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(99), ]), 'price': Validasi.number([ NumberRules.moreThan(0.0), ]), }), ]) ), InlineRule>>((items) { // Calculate total final total = items.fold( 0.0, (sum, item) => sum + (item['price'] * item['quantity']), ); if (total > 10000.0) { return 'Order total exceeds maximum allowed'; } return null; }), ]); final cart = [ { 'product_id': 1, 'name': 'Laptop', 'quantity': 1, 'price': 999.99, }, { 'product_id': 2, 'name': 'Mouse', 'quantity': 2, 'price': 29.99, }, ]; print(cartSchema.validate(cart).isValid); // true ``` ### Coordinate List ```dart final coordinatesSchema = Validasi.list>([ IterableRules.minLength(2, message: 'At least 2 points required'), IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'lat': Validasi.number([ NumberRules.moreThanEqual(-90.0), NumberRules.lessThanEqual(90.0), ]), 'lng': Validasi.number([ NumberRules.moreThanEqual(-180.0), NumberRules.lessThanEqual(180.0), ]), }), ]) ), ]); final route = [ {'lat': 37.7749, 'lng': -122.4194}, // San Francisco {'lat': 34.0522, 'lng': -118.2437}, // Los Angeles {'lat': 40.7128, 'lng': -74.0060}, // New York ]; print(coordinatesSchema.validate(route).isValid); // true ``` ### Tag Cloud ```dart final tagCloudSchema = Validasi.list([ Transform((list) { // Remove duplicates and trim return list?.map((tag) => tag.trim().toLowerCase()) .where((tag) => tag.isNotEmpty) .toSet() .toList(); }), IterableRules.minLength(1, message: 'At least one tag required'), InlineRule>((list) { if (list.length > 20) { return 'Maximum 20 tags allowed'; } return null; }), IterableRules.forEach( Validasi.string([ StringRules.minLength(2), StringRules.maxLength(50), ]) ), ]); final tags = [ ' Flutter ', 'DART', 'flutter', // duplicate 'Mobile', 'dart', // duplicate '', 'UI', ]; final result = tagCloudSchema.validate(tags); print('Valid: ${result.isValid}'); print('Tags: ${result.data}'); // ['flutter', 'dart', 'mobile', 'ui'] ``` ## Custom List Validations ### Unique Elements ```dart final uniqueListSchema = Validasi.list([ IterableRules.minLength(1), InlineRule>((list) { final unique = list.toSet(); if (unique.length != list.length) { return 'List must contain unique elements'; } return null; }), ]); // Valid print(uniqueListSchema.validate(['a', 'b', 'c']).isValid); // true // Invalid - duplicates print(uniqueListSchema.validate(['a', 'b', 'a']).isValid); // false ``` ### Sorted List ```dart final sortedListSchema = Validasi.list([ IterableRules.minLength(2), InlineRule>((list) { for (int i = 1; i < list.length; i++) { if (list[i] < list[i - 1]) { return 'List must be sorted in ascending order'; } } return null; }), ]); // Valid print(sortedListSchema.validate([1, 2, 3, 4]).isValid); // true // Invalid print(sortedListSchema.validate([1, 3, 2, 4]).isValid); // false ``` ### Max Total Size ```dart final sizedListSchema = Validasi.list([ InlineRule>((list) { final totalLength = list.fold(0, (sum, str) => sum + str.length); if (totalLength > 1000) { return 'Total text length exceeds 1000 characters'; } return null; }), ]); ``` ### No Consecutive Duplicates ```dart final noConsecutiveSchema = Validasi.list([ InlineRule>((list) { for (int i = 1; i < list.length; i++) { if (list[i] == list[i - 1]) { return 'No consecutive duplicate values allowed'; } } return null; }), ]); // Valid print(noConsecutiveSchema.validate([1, 2, 1, 3]).isValid); // true // Invalid print(noConsecutiveSchema.validate([1, 2, 2, 3]).isValid); // false ``` ## Error Tracking in Lists ### Individual Element Errors ```dart final emailListSchema = Validasi.list([ IterableRules.forEach( Validasi.string([ InlineRule((value) { return value.contains('@') ? null : 'Invalid email format'; }), ]) ), ]); final result = emailListSchema.validate([ 'valid@example.com', 'invalid-email', 'another@example.com', 'also-invalid', ]); print('Valid: ${result.isValid}'); // false // Print errors with indices for (var error in result.errors) { print('[${error.path?.join('.')}] ${error.message}'); } // Output: // [1] Invalid email format // [3] Invalid email format ``` ### Nested List Errors ```dart final nestedSchema = Validasi.list>([ IterableRules.forEach( Validasi.list([ IterableRules.forEach( Validasi.number([ NumberRules.moreThanEqual(0, message: 'Must be non-negative'), ]) ), ]) ), ]); final result = nestedSchema.validate([ [1, 2, 3], [4, -5, 6], [7, 8, -9], ]); for (var error in result.errors) { print('[${error.path?.join('.')}] ${error.message}'); } // Output: // [1.1] Must be non-negative // [2.2] Must be non-negative ``` ## List with Complex Validation ### Conditional Element Validation ```dart final conditionalListSchema = Validasi.list>([ IterableRules.forEach( Validasi.map([ MapRules.hasFields({ 'type': Validasi.string([ StringRules.oneOf(['text', 'number', 'date']), ]), 'value': Validasi.any([Required()]), }), Having((context, value) { if (value is Map) { final type = value['type']; final val = value['value']; if (type == 'number' && val is! num) { return 'Value must be a number when type is "number"'; } if (type == 'date' && val is! String) { return 'Value must be a string when type is "date"'; } } return null; }), ]) ), ]); final items = [ {'type': 'text', 'value': 'Hello'}, {'type': 'number', 'value': 42}, {'type': 'date', 'value': '2024-01-15'}, ]; print(conditionalListSchema.validate(items).isValid); // true ``` ## Next Steps * [String Validation](/examples/string-validation) * [Number Validation](/examples/number-validation) * [Map Validation](/examples/map-validation) * [Complex Structures](/examples/complex-structures) * [Iterable Rules Reference](/rules/iterable) --- --- url: /validasi/rules/map.md --- # Map Rules Map and object validation rules for structured data. ## MapRules.hasFields() Validates that a map contains specific fields with their own validation schemas. ### Syntax ```dart MapRules.hasFields(Map> validator) ``` ### Parameters * **`T`** - The type of values in the map * **`validator`** (`Map>`, required) - A map of field names to their validation schemas ### Example ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(1)]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), ]), 'age': Validasi.number([ Nullable(), NumberRules.moreThanEqual(0), ]), }), ]); final result = userSchema.validate({ 'name': 'John', 'email': ' USER@EXAMPLE.COM ', 'age': null, }); print(result.isValid); // true print(result.data['email']); // 'user@example.com' ``` ## MapRules.hasFieldKeys() Validates that a map contains all required keys (without validating their values). ### Syntax ```dart MapRules.hasFieldKeys(Set keys) ``` ### Parameters * **`T`** - The type of values in the map * **`keys`** (`Set`, required) - Set of required field names ### Example ```dart final configSchema = Validasi.map([ MapRules.hasFieldKeys({'api_key', 'endpoint', 'timeout'}), ]); // Valid - has all required keys print(configSchema.validate({ 'api_key': 'abc123', 'endpoint': 'https://api.example.com', 'timeout': 30, 'extra': 'allowed', // Extra fields are OK }).isValid); // true // Invalid - missing keys print(configSchema.validate({ 'api_key': 'abc123', }).isValid); // false ``` ## MapRules.conditionalField() Validates a field based on the values of other fields in the map (conditional validation). ### Syntax ```dart MapRules.conditionalField( String field, ConditionalFieldCallback callback ) ``` **Callback Signature:** ```dart typedef ConditionalFieldCallback = String? Function( ValidationContext context, T? value, ); ``` ### Parameters * **`T`** - The type of the field value * **`field`** (String, required) - The name of the field to conditionally validate * **`callback`** (`ConditionalFieldCallback`, required) - Function that returns error message or null ### Example ```dart final orderSchema = Validasi.map([ MapRules.hasFields({ 'is_delivery': Validasi.any(), 'address': Validasi.string([ Nullable(), StringRules.minLength(10), ]), }), MapRules.conditionalField('address', (context, value) { if (context.get('is_delivery') == true && value == null) { return 'Address is required for delivery orders'; } return null; }), ]); // Valid - delivery with address print(orderSchema.validate({ 'is_delivery': true, 'address': '123 Main Street, City', }).isValid); // true // Invalid - delivery without address print(orderSchema.validate({ 'is_delivery': true, 'address': null, }).isValid); // false ``` ## See Also * [Map Validation Examples](/examples/map-validation) * [Complex Structures Examples](/examples/complex-structures) --- --- url: /validasi/examples/map-validation.md --- # Map Validation Examples Learn how to validate maps and objects with Validasi's map validation rules. ## Basic Map Validation ### Simple Object Validation ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final userSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(0), ]), }), ]); // Valid final validUser = {'name': 'John', 'age': 30}; print(userSchema.validate(validUser).isValid); // true // Invalid final invalidUser = {'name': '', 'age': -1}; final result = userSchema.validate(invalidUser); print(result.isValid); // false for (var error in result.errors) { print('[${error.path?.join('.')}] ${error.message}'); } ``` ### Required Keys Validation ```dart final configSchema = Validasi.map([ MapRules.hasFieldKeys({'api_key', 'endpoint', 'timeout'}), ]); // Valid print(configSchema.validate({ 'api_key': 'abc123', 'endpoint': 'https://api.example.com', 'timeout': 30, 'extra': 'allowed', // Extra fields are OK }).isValid); // true // Invalid - missing keys print(configSchema.validate({ 'api_key': 'abc123', }).isValid); // false ``` ## Nested Map Validation ### One Level Nesting ```dart final profileSchema = Validasi.map([ MapRules.hasFields({ 'user': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(1)]), 'email': Validasi.string([StringRules.minLength(5)]), }), ]), 'settings': Validasi.map([ MapRules.hasFields({ 'theme': Validasi.string([ StringRules.oneOf(['light', 'dark', 'auto']), ]), 'notifications': Validasi.any(), }), ]), }), ]); final profile = { 'user': { 'name': 'John Doe', 'email': 'john@example.com', }, 'settings': { 'theme': 'dark', 'notifications': true, }, }; print(profileSchema.validate(profile).isValid); // true ``` ### Deep Nesting ```dart final addressSchema = Validasi.map([ MapRules.hasFields({ 'street': Validasi.string([StringRules.minLength(1)]), 'city': Validasi.string([StringRules.minLength(1)]), 'country': Validasi.string([StringRules.minLength(1)]), 'coordinates': Validasi.map([ MapRules.hasFields({ 'lat': Validasi.number([ NumberRules.moreThanEqual(-90.0), NumberRules.lessThanEqual(90.0), ]), 'lng': Validasi.number([ NumberRules.moreThanEqual(-180.0), NumberRules.lessThanEqual(180.0), ]), }), ]), }), ]); final address = { 'street': '123 Main St', 'city': 'San Francisco', 'country': 'USA', 'coordinates': { 'lat': 37.7749, 'lng': -122.4194, }, }; print(addressSchema.validate(address).isValid); // true ``` ## Conditional Field Validation ### Basic Conditional Validation ```dart final orderSchema = Validasi.map([ MapRules.hasFields({ 'is_delivery': Validasi.any(), 'address': Validasi.string([ Nullable(), StringRules.minLength(10), ]), }), MapRules.conditionalField('address', (context, value) { if (context.get('is_delivery') == true && value == null) { return 'Address is required for delivery orders'; } return null; }), ]); // Valid - delivery with address print(orderSchema.validate({ 'is_delivery': true, 'address': '123 Main Street, City', }).isValid); // true // Valid - pickup without address print(orderSchema.validate({ 'is_delivery': false, 'address': null, }).isValid); // true // Invalid - delivery without address print(orderSchema.validate({ 'is_delivery': true, 'address': null, }).isValid); // false ``` ### Multiple Conditional Fields ```dart final paymentSchema = Validasi.map([ MapRules.hasFields({ 'payment_method': Validasi.string([ StringRules.oneOf(['card', 'bank', 'cash']), ]), 'card_number': Validasi.string([Nullable()]), 'bank_account': Validasi.string([Nullable()]), }), MapRules.conditionalField('card_number', (context, value) { if (context.get('payment_method') == 'card' && value == null) { return 'Card number is required for card payments'; } return null; }), MapRules.conditionalField('bank_account', (context, value) { if (context.get('payment_method') == 'bank' && value == null) { return 'Bank account is required for bank transfers'; } return null; }), ]); // Valid - card payment with card number print(paymentSchema.validate({ 'payment_method': 'card', 'card_number': '4111111111111111', 'bank_account': null, }).isValid); // true // Valid - cash payment print(paymentSchema.validate({ 'payment_method': 'cash', 'card_number': null, 'bank_account': null, }).isValid); // true // Invalid - card payment without card number print(paymentSchema.validate({ 'payment_method': 'card', 'card_number': null, 'bank_account': null, }).isValid); // false ``` ## Optional Fields ### Nullable Fields ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([StringRules.minLength(1)]), 'nickname': Validasi.string([ Nullable(), StringRules.minLength(2), ]), 'age': Validasi.number([ Nullable(), NumberRules.moreThanEqual(0), ]), }), ]); // Valid with nulls print(userSchema.validate({ 'name': 'John Doe', 'nickname': null, 'age': null, }).isValid); // true // Valid with values print(userSchema.validate({ 'name': 'John Doe', 'nickname': 'JD', 'age': 30, }).isValid); // true ``` ## Map Transformations ### Transform Field Values ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), ]), 'name': Validasi.string([ Transform((value) => value?.trim()), Transform((value) { // Capitalize first letter of each word return value?.split(' ') .map((word) => word.isEmpty ? '' : word[0].toUpperCase() + word.substring(1).toLowerCase()) .join(' '); }), ]), }), ]); final result = userSchema.validate({ 'email': ' USER@EXAMPLE.COM ', 'name': 'john doe', }); print(result.data); // { // email: 'user@example.com', // name: 'John Doe' // } ``` ## Real-World Examples ### Registration Form ```dart final registrationSchema = Validasi.map([ MapRules.hasFields({ 'username': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(3), StringRules.maxLength(20), InlineRule((value) { if (!RegExp(r'^[a-z0-9_]+$').hasMatch(value)) { return 'Username can only contain letters, numbers, and underscores'; } return null; }), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), InlineRule((value) { if (!value.contains('@')) { return 'Invalid email format'; } return null; }), ]), 'password': Validasi.string([ StringRules.minLength(8), InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Password must contain uppercase letter'; } if (!value.contains(RegExp(r'[0-9]'))) { return 'Password must contain number'; } return null; }), ]), 'age': Validasi.number([ Nullable(), NumberRules.moreThanEqual(13), ]), 'terms': Validasi.any([ InlineRule((value) { return value == true ? null : 'You must accept the terms'; }), ]), }), ]); final formData = { 'username': 'JohnDoe123', 'email': 'JOHN@EXAMPLE.COM', 'password': 'SecurePass123', 'age': 25, 'terms': true, }; final result = registrationSchema.validate(formData); print('Valid: ${result.isValid}'); print('Processed data: ${result.data}'); ``` ### API Request Validation ```dart final apiRequestSchema = Validasi.map([ MapRules.hasFields({ 'endpoint': Validasi.string([ StringRules.minLength(1), ]), 'method': Validasi.string([ StringRules.oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), ]), 'headers': Validasi.map([ Nullable(), ]), 'body': Validasi.any([ Nullable(), ]), 'timeout': Validasi.number([ Nullable(), NumberRules.moreThan(0), NumberRules.lessThanEqual(300), ]), }), ]); final request = { 'endpoint': '/api/users', 'method': 'POST', 'headers': { 'Content-Type': 'application/json', 'Authorization': 'Bearer token123', }, 'body': { 'name': 'John Doe', 'email': 'john@example.com', }, 'timeout': 30, }; print(apiRequestSchema.validate(request).isValid); // true ``` ### Product Validation ```dart final productSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1), StringRules.maxLength(100), ]), 'description': Validasi.string([ StringRules.minLength(10), StringRules.maxLength(500), ]), 'price': Validasi.number([ NumberRules.moreThan(0.0), ]), 'stock': Validasi.number([ NumberRules.moreThanEqual(0), ]), 'category': Validasi.string([ StringRules.oneOf(['electronics', 'clothing', 'books', 'food']), ]), 'tags': Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([StringRules.minLength(2)]), ), ]), 'on_sale': Validasi.any(), 'discount': Validasi.number([ Nullable(), NumberRules.moreThanEqual(0.0), NumberRules.lessThan(100.0), ]), }), MapRules.conditionalField('discount', (context, value) { if (context.get('on_sale') == true && value == null) { return 'Discount is required when product is on sale'; } return null; }), ]); final product = { 'name': 'Wireless Headphones', 'description': 'High-quality wireless headphones with noise cancellation', 'price': 99.99, 'stock': 50, 'category': 'electronics', 'tags': ['audio', 'wireless', 'bluetooth'], 'on_sale': true, 'discount': 15.0, }; print(productSchema.validate(product).isValid); // true ``` ### Blog Post Validation ```dart final blogPostSchema = Validasi.map([ MapRules.hasFields({ 'title': Validasi.string([ StringRules.minLength(5), StringRules.maxLength(200), ]), 'slug': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), Transform((value) => value?.replaceAll(RegExp(r'\s+'), '-')), StringRules.minLength(5), InlineRule((value) { if (!RegExp(r'^[a-z0-9-]+$').hasMatch(value)) { return 'Slug can only contain lowercase letters, numbers, and hyphens'; } return null; }), ]), 'content': Validasi.string([ StringRules.minLength(100), ]), 'author': Validasi.map([ MapRules.hasFields({ 'id': Validasi.number([NumberRules.moreThan(0)]), 'name': Validasi.string([StringRules.minLength(1)]), }), ]), 'tags': Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(2), ]), ), ]), 'published': Validasi.any(), 'published_at': Validasi.string([ Nullable(), ]), }), MapRules.conditionalField('published_at', (context, value) { if (context.get('published') == true && value == null) { return 'Published date is required when post is published'; } return null; }), ]); final blogPost = { 'title': 'Getting Started with Flutter', 'slug': 'Getting Started With Flutter', 'content': 'Flutter is an amazing framework...' + ('.' * 100), 'author': { 'id': 123, 'name': 'John Doe', }, 'tags': ['Flutter', 'Mobile', 'Development'], 'published': true, 'published_at': '2024-01-15T10:00:00Z', }; final result = blogPostSchema.validate(blogPost); print('Valid: ${result.isValid}'); print('Slug: ${result.data['slug']}'); // 'getting-started-with-flutter' ``` ### Settings Validation ```dart final settingsSchema = Validasi.map([ MapRules.hasFields({ 'appearance': Validasi.map([ MapRules.hasFields({ 'theme': Validasi.string([ StringRules.oneOf(['light', 'dark', 'auto']), ]), 'font_size': Validasi.number([ NumberRules.moreThanEqual(8), NumberRules.lessThanEqual(32), ]), }), ]), 'notifications': Validasi.map([ MapRules.hasFields({ 'email': Validasi.any(), 'push': Validasi.any(), 'sms': Validasi.any(), }), ]), 'privacy': Validasi.map([ MapRules.hasFields({ 'profile_visible': Validasi.any(), 'show_email': Validasi.any(), }), ]), }), ]); final settings = { 'appearance': { 'theme': 'dark', 'font_size': 14, }, 'notifications': { 'email': true, 'push': true, 'sms': false, }, 'privacy': { 'profile_visible': true, 'show_email': false, }, }; print(settingsSchema.validate(settings).isValid); // true ``` ## Custom Map Validation ### Cross-Field Validation ```dart final passwordMatchSchema = Validasi.map([ MapRules.hasFields({ 'password': Validasi.string([StringRules.minLength(8)]), 'confirm_password': Validasi.string([StringRules.minLength(8)]), }), Having((context, value) { if (value is Map && value['password'] != value['confirm_password']) { return 'Passwords do not match'; } return null; }), ]); // Valid print(passwordMatchSchema.validate({ 'password': 'SecurePass123', 'confirm_password': 'SecurePass123', }).isValid); // true // Invalid print(passwordMatchSchema.validate({ 'password': 'SecurePass123', 'confirm_password': 'DifferentPass', }).isValid); // false ``` ### Date Range Validation ```dart final dateRangeSchema = Validasi.map([ MapRules.hasFields({ 'start_date': Validasi.string([StringRules.minLength(1)]), 'end_date': Validasi.string([StringRules.minLength(1)]), }), Having((context, value) { if (value is Map) { final start = DateTime.tryParse(value['start_date'] ?? ''); final end = DateTime.tryParse(value['end_date'] ?? ''); if (start != null && end != null && end.isBefore(start)) { return 'End date must be after start date'; } } return null; }), ]); ``` ## Error Handling in Maps ```dart final schema = Validasi.map([ MapRules.hasFields({ 'user': Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1, message: 'Name is required'), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(0, message: 'Age must be positive'), ]), }), ]), }), ]); final result = schema.validate({ 'user': { 'name': '', 'age': -5, }, }); for (var error in result.errors) { print('[${error.path?.join('.')}] ${error.message}'); } // Output: // [user.name] Name is required // [user.age] Age must be positive ``` ## Next Steps * [Complex Structures](/examples/complex-structures) * [Map Rules Reference](/rules/map) * [Transformations Guide](/guide/transformations) --- --- url: /validasi/markdown-examples.md --- # Markdown Extension Examples This page demonstrates some of the built-in markdown extensions provided by VitePress. ## Syntax Highlighting VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: **Input** ````md ```js{4} export default { data () { return { msg: 'Highlighted!' } } } ``` ```` **Output** ```js{4} export default { data () { return { msg: 'Highlighted!' } } } ``` ## Custom Containers **Input** ```md ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ::: details This is a details block. ::: ``` **Output** ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ::: details This is a details block. ::: ## More Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). --- --- url: /validasi/v0/extending/new-validation.md --- # New Validation You can create a new validation by extending the `Validator` class. The `Validator` class is an abstract class that contains a method to validate the input value. The following code shows how to create a new validation: ```dart import 'package:validasi/validasi.dart'; class MyValidator extends Validator { @override StringValidator nullable() => super.nullable(); @override StringValidator custom(CustomCallback callback) => super.custom(callback); @override StringValidator customFor(CustomRule customRule) => super.customFor(customRule); } ``` That's it! That's all you need to create a new validation. You can now use the `MyValidator` class to validate the input value. ## Adding Rule You can add a new rule by adding a new method to your class. Following the convention of Validasi, the method name should match the rule name, and the method should return the same type as the class. The following code shows how to add a new rule to the `MyValidator` class: ```dart import 'package:validasi/validasi.dart'; class MyValidator extends Validator { MyValidator myRule() { addRule( name: 'myRule', test: (value) => value == 'myValue', message: ':name must be myValue', ); return this; } } ``` The rule will be registered using the `addRule` method. The `addRule` method requires three parameters: * `name`: The name of the rule. * `test`: The test function to validate the input value. * `message`: The error message to return if the test function fails. Similarly to [customizing default error message](/v0/guide/basic-concept.html#replacing-default-message) you can also use `:name` here! ## Adding Transformer Support In order to add [Transformer](/v0/guide/transformer) support to your custom validator, you can inherit the `transformer` property from the `Validator` class. ```dart import 'package:validasi/validasi.dart'; class MyValidator extends Validator { MyValidator({super.transformer}); } ``` That's it! ## Using your Custom Validator In order to use your Custom Validator just call the class like how you did it with normal Dart. ```dart MyValidator myValidator = MyValidator(); myValidator.myRule().parse('myValue'); ``` Your Custom Validator can also be passed to sub-type of `Validator` like [`ArrayValidator`](/v0/types/array) and [`ObjectValidator`](/v0/types/object). ```dart Validasi.array(MyValidator()); Validasi.object({ 'name': MyValidator(), }); ``` ## Adding to Validasi Now if you want to add your custom validator to Validasi, you have to extends the `Validasi` class and add a new method to return your custom validator. ::: code-group ```dart [utils/validation.dart] import 'package:validasi/validasi.dart'; class MyValidasi extends Validasi { MyValidator myValidator() => MyValidator(); } ``` ```dart [main.dart] import 'package:app/utils/validation.dart'; void main() { MyValidator.myValidator().myRule().parse('myValue'); } ``` ::: code-group As of currently, Dart doesn't support static method extensions, see [this issue](https://github.com/dart-lang/language/issues/723) for more information. --- --- url: /validasi/v0/types/number.md --- # Number The `Number` schema is used to validate the input value as a number. This schema contains some useful built-in validators to validate the input value. The following code shows how to create a `Number` schema: ::: code-group ```dart [Using Validasi] Validasi.number(); ``` ```dart [Using Direct Class] NumberValidator(); ``` ::: ::: info The `Number` schema is based on [`num`](https://api.dart.dev/stable/3.5.3/dart-core/num-class.html). Which could represent both `int` and `double` values. ::: Below are the available methods for the `Number` schema: \[\[toc]] ## nonDecimal ```dart Validasi.number().nonDecimal({String? message}); ``` The `nonDecimal` method is used to validate that the input value is a non-decimal number. This method will return an error message if the input value is a decimal number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().nonDecimal(); final result = schema.tryParse(3.14); print(result.errors.first.message); // 'field must be non-decimal number' } ``` ## decimal ```dart Validasi.number().decimal({String? message}); ``` The `decimal` method is used to validate that the input value is a decimal number. This method will return an error message if the input value is not a decimal number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().decimal(); final result = schema.tryParse(3); print(result.errors.first.message); // 'field must be decimal number' } ``` ## positive ```dart Validasi.number().positive({String? message}); ``` The `positive` method is used to validate that the input value is a positive number. This method will return an error message if the input value is not a positive number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().positive(); final result = schema.tryParse(-1); print(result.errors.first.message); // 'field must be positive number' } ``` ## negative ```dart Validasi.number().negative({String? message}); ``` The `negative` method is used to validate that the input value is a negative number. This method will return an error message if the input value is not a negative number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().negative(); final result = schema.tryParse(1); print(result.errors.first.message); // 'field must be negative number' } ``` ## nonPositive ```dart Validasi.number().nonPositive({String? message}); ``` The `nonPositive` method is used to validate that the input value is a non-positive number. This method will return an error message if the input value is a positive number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().nonPositive(); final result = schema.tryParse(1); print(result.errors.first.message); // 'field must be non-positive number' } ``` ## nonNegative ```dart Validasi.number().nonNegative({String? message}); ``` The `nonNegative` method is used to validate that the input value is a non-negative number. This method will return an error message if the input value is a negative number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().nonNegative(); final result = schema.tryParse(-1); print(result.errors.first.message); // 'field must be non-negative number' } ``` ## gt ```dart Validasi.number().gt(num min, {String? message}); ``` The `gt` method is used to validate that the input value is greater than the specified minimum value. This method will return an error message if the input value is not greater than the specified minimum value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().gt(5); final result = schema.tryParse(3); print(result.errors.first.message); // 'field must be greater than 5' } ``` ## gte ```dart Validasi.number().gte(num min, {String? message}); ``` The `gte` method is used to validate that the input value is greater than or equal to the specified minimum value. This method will return an error message if the input value is not greater than or equal to the specified minimum value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().gte(5); final result = schema.tryParse(3); print(result.errors.first.message); // 'field must be greater than or equal to 5' } ``` ## lt ```dart Validasi.number().lt(num max, {String? message}); ``` The `lt` method is used to validate that the input value is less than the specified maximum value. This method will return an error message if the input value is not less than the specified maximum value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().lt(5); final result = schema.tryParse(7); print(result.errors.first.message); // 'field must be less than 5' } ``` ## lte ```dart Validasi.number().lte(num max, {String? message}); ``` The `lte` method is used to validate that the input value is less than or equal to the specified maximum value. This method will return an error message if the input value is not less than or equal to the specified maximum value. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().lte(5); final result = schema.tryParse(7); print(result.errors.first.message); // 'field must be less than or equal to 5' } ``` ## finite ```dart Validasi.number().finite({String? message}); ``` The `finite` method is used to validate that the input value is a finite number. This method will return an error message if the input value is not a finite number. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number().finite(); final result = schema.tryParse(double.infinity); print(result.errors.first.message); // 'field must be finite number' } ``` --- --- url: /validasi/rules/number.md --- # Number Rules Number validation rules for integer and double validation. ## NumberRules.moreThan() Validates that a number is strictly greater than a specified value. ### Syntax ```dart NumberRules.moreThan(T value, {String? message}) ``` ### Parameters * **`value`** (T extends num, required) - The value that the input must be greater than * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be more than {value}" ``` ### Example ```dart final priceSchema = Validasi.number([ NumberRules.moreThan(0.0, message: 'Price must be positive'), ]); print(priceSchema.validate(0.01).isValid); // true print(priceSchema.validate(0.0).isValid); // false ``` ## NumberRules.moreThanEqual() Validates that a number is greater than or equal to a specified value. ### Syntax ```dart NumberRules.moreThanEqual(T value, {String? message}) ``` ### Parameters * **`value`** (T extends num, required) - The minimum value allowed (inclusive) * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be more than or equal to {value}" ``` ### Example ```dart final stockSchema = Validasi.number([ NumberRules.moreThanEqual(0, message: 'Stock cannot be negative'), ]); print(stockSchema.validate(0).isValid); // true print(stockSchema.validate(-1).isValid); // false ``` ## NumberRules.lessThan() Validates that a number is strictly less than a specified value. ### Syntax ```dart NumberRules.lessThan(T value, {String? message}) ``` ### Parameters * **`value`** (T extends num, required) - The value that the input must be less than * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be less than {value}" ``` ### Example ```dart final temperatureSchema = Validasi.number([ NumberRules.lessThan(100.0, message: 'Temperature must be below boiling point'), ]); print(temperatureSchema.validate(99.9).isValid); // true print(temperatureSchema.validate(100.0).isValid); // false ``` ## NumberRules.lessThanEqual() Validates that a number is less than or equal to a specified value. ### Syntax ```dart NumberRules.lessThanEqual(T value, {String? message}) ``` ### Parameters * **`value`** (T extends num, required) - The maximum value allowed (inclusive) * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be less than or equal to {value}" ``` ### Example ```dart final percentSchema = Validasi.number([ NumberRules.lessThanEqual(100.0, message: 'Percentage cannot exceed 100'), ]); print(percentSchema.validate(100.0).isValid); // true print(percentSchema.validate(100.1).isValid); // false ``` ## NumberRules.finite() Validates that a number is finite (not infinity or NaN). ### Syntax ```dart NumberRules.finite({String? message}) ``` ### Parameters * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be finite" ``` ### Example ```dart final schema = Validasi.number([ NumberRules.finite(message: 'Must be a valid number'), ]); print(schema.validate(42.0).isValid); // true print(schema.validate(double.infinity).isValid); // false print(schema.validate(double.nan).isValid); // false ``` ## See Also * [Number Validation Examples](/examples/number-validation) --- --- url: /validasi/examples/number-validation.md --- # Number Validation Examples Learn how to validate numbers (integers and doubles) with Validasi's number validation rules. ## Basic Number Validation ### Integer Validation ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final ageSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(150), ]); // Valid print(ageSchema.validate(25).isValid); // true print(ageSchema.validate(0).isValid); // true // Invalid print(ageSchema.validate(-1).isValid); // false print(ageSchema.validate(200).isValid); // false ``` ### Double Validation ```dart final priceSchema = Validasi.number([ NumberRules.moreThan(0.0), NumberRules.lessThan(1000000.0), ]); // Valid print(priceSchema.validate(99.99).isValid); // true print(priceSchema.validate(0.01).isValid); // true // Invalid print(priceSchema.validate(0.0).isValid); // false print(priceSchema.validate(-10.5).isValid); // false ``` ## Comparison Rules ### Greater Than ```dart final positiveSchema = Validasi.number([ NumberRules.moreThan(0), ]); print(positiveSchema.validate(1).isValid); // true print(positiveSchema.validate(0).isValid); // false print(positiveSchema.validate(-1).isValid); // false ``` ### Greater Than or Equal ```dart final nonNegativeSchema = Validasi.number([ NumberRules.moreThanEqual(0), ]); print(nonNegativeSchema.validate(0).isValid); // true print(nonNegativeSchema.validate(1).isValid); // true print(nonNegativeSchema.validate(-1).isValid); // false ``` ### Less Than ```dart final belowHundredSchema = Validasi.number([ NumberRules.lessThan(100), ]); print(belowHundredSchema.validate(99).isValid); // true print(belowHundredSchema.validate(100).isValid); // false print(belowHundredSchema.validate(101).isValid); // false ``` ### Less Than or Equal ```dart final maxHundredSchema = Validasi.number([ NumberRules.lessThanEqual(100), ]); print(maxHundredSchema.validate(100).isValid); // true print(maxHundredSchema.validate(99).isValid); // true print(maxHundredSchema.validate(101).isValid); // false ``` ## Range Validation ### Simple Range ```dart final percentageSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(100), ]); // Valid print(percentageSchema.validate(0).isValid); // true print(percentageSchema.validate(50).isValid); // true print(percentageSchema.validate(100).isValid); // true // Invalid print(percentageSchema.validate(-1).isValid); // false print(percentageSchema.validate(101).isValid); // false ``` ### Exclusive Range ```dart final temperatureSchema = Validasi.number([ NumberRules.moreThan(-273.15), // Above absolute zero NumberRules.lessThan(1000.0), ]); print(temperatureSchema.validate(20.5).isValid); // true print(temperatureSchema.validate(-273.15).isValid); // false ``` ## Finite Number Validation ### Check for Finite Values ```dart final finiteSchema = Validasi.number([ NumberRules.finite(), ]); // Valid print(finiteSchema.validate(42.0).isValid); // true print(finiteSchema.validate(-123.456).isValid); // true print(finiteSchema.validate(0.0).isValid); // true // Invalid print(finiteSchema.validate(double.infinity).isValid); // false print(finiteSchema.validate(double.negativeInfinity).isValid); // false print(finiteSchema.validate(double.nan).isValid); // false ``` ### Combined with Range ```dart final safeDoubleSchema = Validasi.number([ NumberRules.finite(), NumberRules.moreThanEqual(0.0), ]); print(safeDoubleSchema.validate(123.45).isValid); // true print(safeDoubleSchema.validate(double.infinity).isValid); // false print(safeDoubleSchema.validate(-5.0).isValid); // false ``` ## Number Transformations ### Absolute Value ```dart final absSchema = Validasi.number([ Transform((value) => value?.abs()), NumberRules.lessThanEqual(100), ]); final result = absSchema.validate(-50); print(result.isValid); // true print(result.data); // 50 ``` ### Rounding ```dart final roundedSchema = Validasi.number([ Transform((value) => value?.roundToDouble()), ]); final result = roundedSchema.validate(3.7); print(result.data); // 4.0 final result2 = roundedSchema.validate(3.2); print(result2.data); // 3.0 ``` ### Clamping ```dart final clampedSchema = Validasi.number([ Transform((value) => value?.clamp(0, 100)), ]); print(clampedSchema.validate(-10).data); // 0 print(clampedSchema.validate(50).data); // 50 print(clampedSchema.validate(150).data); // 100 ``` ### Precision Control ```dart final precisionSchema = Validasi.number([ Transform((value) { if (value == null) return null; return (value * 100).round() / 100; // 2 decimal places }), ]); final result = precisionSchema.validate(3.14159); print(result.data); // 3.14 ``` ## Nullable Numbers ```dart final optionalAgeSchema = Validasi.number([ Nullable(), NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(150), ]); // Valid print(optionalAgeSchema.validate(null).isValid); // true print(optionalAgeSchema.validate(25).isValid); // true // Invalid print(optionalAgeSchema.validate(-1).isValid); // false ``` ## Real-World Examples ### Price Validation ```dart final priceSchema = Validasi.number([ NumberRules.finite(), NumberRules.moreThan(0.0), NumberRules.lessThan(1000000.0), Transform((value) { // Round to 2 decimal places if (value == null) return null; return (value * 100).round() / 100; }), ]); final result = priceSchema.validate(19.999); print('Valid: ${result.isValid}'); print('Price: \$${result.data}'); // $20.0 ``` ### Rating System ```dart final ratingSchema = Validasi.number([ NumberRules.moreThanEqual(1), NumberRules.lessThanEqual(5), InlineRule((value) { final validRatings = [1, 2, 3, 4, 5]; return validRatings.contains(value) ? null : 'Rating must be between 1 and 5'; }), ]); // Valid print(ratingSchema.validate(5).isValid); // true // Invalid print(ratingSchema.validate(0).isValid); // false print(ratingSchema.validate(6).isValid); // false ``` ### Percentage Calculation ```dart final percentSchema = Validasi.number([ NumberRules.moreThanEqual(0.0), NumberRules.lessThanEqual(100.0), Transform((value) { // Round to 1 decimal place if (value == null) return null; return (value * 10).round() / 10; }), ]); final result = percentSchema.validate(75.678); print('Percentage: ${result.data}%'); // 75.7% ``` ### Temperature Validation ```dart final celsiusSchema = Validasi.number([ NumberRules.moreThan(-273.15), // Absolute zero NumberRules.lessThan(1000.0), InlineRule((value) { if (value < -100 || value > 100) { return 'Unusual temperature value'; } return null; }), ]); print(celsiusSchema.validate(25.5).isValid); // true print(celsiusSchema.validate(-273.15).isValid); // false ``` ### Age Validation ```dart final ageSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(150), InlineRule((value) { if (value < 18) { return 'Must be at least 18 years old'; } return null; }), ]); print(ageSchema.validate(25).isValid); // true print(ageSchema.validate(15).isValid); // false ``` ### Quantity Validation ```dart final quantitySchema = Validasi.number([ NumberRules.moreThan(0), NumberRules.lessThanEqual(999), InlineRule((value) { if (value > 100) { return 'Large orders require special handling'; } return null; }), ]); print(quantitySchema.validate(5).isValid); // true print(quantitySchema.validate(150).isValid); // false (warning) ``` ### Discount Validation ```dart final discountSchema = Validasi.number([ NumberRules.moreThanEqual(0.0), NumberRules.lessThan(100.0), // Can't discount 100% Transform((value) { // Ensure max 2 decimal places if (value == null) return null; return (value * 100).round() / 100; }), ]); final result = discountSchema.validate(15.999); print('Discount: ${result.data}%'); // 16.0% ``` ### Coordinate Validation ```dart final latitudeSchema = Validasi.number([ NumberRules.moreThanEqual(-90.0), NumberRules.lessThanEqual(90.0), ]); final longitudeSchema = Validasi.number([ NumberRules.moreThanEqual(-180.0), NumberRules.lessThanEqual(180.0), ]); // Valid coordinates print(latitudeSchema.validate(37.7749).isValid); // true (SF) print(longitudeSchema.validate(-122.4194).isValid); // true (SF) // Invalid print(latitudeSchema.validate(100.0).isValid); // false ``` ## Custom Number Validations ### Even Numbers Only ```dart final evenSchema = Validasi.number([ InlineRule((value) { return value % 2 == 0 ? null : 'Must be an even number'; }), ]); print(evenSchema.validate(4).isValid); // true print(evenSchema.validate(5).isValid); // false ``` ### Multiple Of ```dart final multipleOfFiveSchema = Validasi.number([ InlineRule((value) { return value % 5 == 0 ? null : 'Must be a multiple of 5'; }), ]); print(multipleOfFiveSchema.validate(10).isValid); // true print(multipleOfFiveSchema.validate(7).isValid); // false ``` ### Prime Numbers ```dart bool isPrime(int n) { if (n < 2) return false; for (int i = 2; i <= n ~/ 2; i++) { if (n % i == 0) return false; } return true; } final primeSchema = Validasi.number([ InlineRule((value) { return isPrime(value) ? null : 'Must be a prime number'; }), ]); print(primeSchema.validate(7).isValid); // true print(primeSchema.validate(8).isValid); // false ``` ### Power of Two ```dart final powerOfTwoSchema = Validasi.number([ NumberRules.moreThan(0), InlineRule((value) { // Check if value is a power of 2 return (value & (value - 1)) == 0 ? null : 'Must be a power of 2'; }), ]); print(powerOfTwoSchema.validate(8).isValid); // true print(powerOfTwoSchema.validate(10).isValid); // false ``` ## String to Number Conversion ### Parse Integer ```dart final parseIntSchema = Validasi.string([ Transform((value) { if (value == null) return null; return int.tryParse(value); }), ]).pipe( Validasi.number([ Required(message: 'Invalid number format'), NumberRules.moreThanEqual(0), ]) ); final result = parseIntSchema.validate('42'); print('Valid: ${result.isValid}'); print('Number: ${result.data}'); // 42 ``` ### Parse Double with Validation ```dart final parseDoubleSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) { if (value == null) return null; return double.tryParse(value); }), ]).pipe( Validasi.number([ Required(message: 'Invalid decimal number'), NumberRules.finite(), NumberRules.moreThanEqual(0.0), ]) ); final result = parseDoubleSchema.validate(' 19.99 '); print('Valid: ${result.isValid}'); print('Price: ${result.data}'); // 19.99 ``` ## Error Messages ### Custom Error Messages ```dart final customSchema = Validasi.number([ NumberRules.moreThanEqual( 0, message: 'Value cannot be negative' ), NumberRules.lessThanEqual( 100, message: 'Value cannot exceed 100' ), ]); final result = customSchema.validate(-5); for (var error in result.errors) { print(error.message); // "Value cannot be negative" } ``` ### Context-Aware Messages ```dart final scoreSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThanEqual(100), InlineRule((value) { if (value < 50) { return 'Score is below passing grade (50)'; } if (value < 70) { return 'Score is acceptable but could be improved'; } return null; // Score is good }), ]); ``` ## Next Steps * [String Validation](/examples/string-validation) * [List Validation](/examples/list-validation) * [Map Validation](/examples/map-validation) * [Complex Structures](/examples/complex-structures) * [Number Rules Reference](/rules/number) --- --- url: /validasi/v0/types/object.md --- # Object The `Object` schema is used to validate the input value as an object or `Map`. This schema contains some useful built-in validators to validate the input value. The following code shows how to create an `Object` schema: ```dart [Using Validasi] Validasi.object({ 'name': Validasi.string(), 'age': Validasi.number(), }); ``` ```dart [Using Direct Class] ObjectValidator({ 'name': StringValidator(), 'age': NumberValidator(), }); ``` The `Object` schema requires a schema parameter a map of string-validator pairs to validate the keys and values in the object. You can also have nesting schemas in the `Object` schema. The following code shows how to create a nested `Object` schema: ```dart Validasi.object({ 'name': Validasi.string(), 'address': Validasi.object({ 'street': Validasi.string(), 'city': Validasi.string(), }), }); ``` Or combine them with other schema: ```dart Validasi.object({ 'name': Validasi.string(), 'addresses': Validasi.array( Validasi.object({ 'street': Validasi.string(), 'city': Validasi.string(), }), ), }); ``` ::: info The Array schema will reconstruct your input value accoring to the schema you provided. This allow you to take advantage of transformer in the schema to convert the array value into the desired format. ```dart Validasi.object({ 'name': Validasi.string(), 'numbers': Validasi.array(Validasi.number(transformer: NumberTransformer())), }); ``` Above code can takes `name: string, numbers: List` and returning `name: string, numbers: List`. The `numbers` key will be converted according to the transformer you provided. ::: ## The `null` Behaviour Since Object Validator will reconstruct your input value, it will replace the value with `null` if the value fail to validate. This is to ensure the object length is still the same as the input value. However, for some this behaviour might confuse you. Therefore, this guide will attempt to explain when you will receive `null` and kinds. ### When Receive `null` If the input value is null and not an object, the reconstructed object will return null as well. ```dart var schema = Validasi.object({ 'name': Validasi.string(), 'age': Validasi.number(), }); print(schema.tryParse(null).value); // null ``` ### When receive `object` but empty If the input value is an object but empty, the reconstructed object will differs compared to receiving `null`. It will return an object with all keys and values set to `null`. ```dart var schema = Validasi.object({ 'name': Validasi.string(), 'age': Validasi.number(), }); print(schema.tryParse({}).value); // {name: null, age: null} ``` ## Available Methods The `Object` schema has the following methods: ### `extend` The `extend` method is used to add more keys and validators to the object schema. The following code shows how to use the `extend` method: ```dart var schema = Validasi.object({ 'name': Validasi.string(), }); schema.extend({ 'age': Validasi.number(), }); print(schema.tryParse({'name': 'John', 'age': 20}).value); // {name: John, age: 20} ``` ::: info The `extend` method is immutable, which means it will return a new object schema with the new keys and validators added. ::: ### `without` The `without` method is used to remove keys from the object schema. The following code shows how to use the `without` method: ```dart var schema = Validasi.object({ 'name': Validasi.string(), 'age': Validasi.number(), }); schema.without('age'); print(schema.tryParse({'name': 'John', 'age': 20}).value); // {name: John} ``` ::: info The `without` method is immutable, which means it will return a new object schema with the keys removed. ::: --- --- url: /validasi/guide/quick-start.md --- # Quick Start Let's build your first validation schema with Validasi! This guide will walk you through the basics and get you validating data in minutes. ## Basic String Validation The simplest validation checks if a string meets certain criteria: ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; void main() { // Create a schema final nameSchema = Validasi.string([ StringRules.minLength(2), StringRules.maxLength(50), ]); // Validate data final result = nameSchema.validate('John Doe'); if (result.isValid) { print('✓ Valid name: ${result.data}'); } else { print('✗ Errors: ${result.errors.map((e) => e.message).join(', ')}'); } } ``` ## Number Validation Validate numeric values with range checks: ```dart final ageSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThan(150), ]); final result = ageSchema.validate(25); print('Age is valid: ${result.isValid}'); ``` ## Nullable Fields Handle optional fields with the `Nullable` modifier: ```dart final optionalEmailSchema = Validasi.string([ Nullable(), StringRules.email(), ]); // Both are valid print(optionalEmailSchema.validate(null).isValid); // true print(optionalEmailSchema.validate('test@example.com').isValid); // true ``` ## Map Validation Validate complex objects with nested fields: ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(2), ]), 'email': Validasi.string([ StringRules.email(), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(18), ]), }), ]); final userData = { 'name': 'Alice Smith', 'email': 'alice@example.com', 'age': 28, }; final result = userSchema.validate(userData); if (result.isValid) { print('User data is valid!'); print('Validated data: ${result.data}'); } else { for (var error in result.errors) { print('Error at ${error.path?.join('.')}: ${error.message}'); } } ``` ## List Validation Validate arrays and collections: ```dart final tagsSchema = Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([ StringRules.minLength(2), StringRules.maxLength(20), ]), ), ]); final result = tagsSchema.validate(['flutter', 'dart', 'validation']); print('Tags are valid: ${result.isValid}'); ``` ## Data Transformation Transform values during validation: ```dart final trimmedNameSchema = Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(2), ]); final result = trimmedNameSchema.validate(' John '); print('Trimmed name: ${result.data}'); // Output: "John" ``` ## Custom Validation Create custom validation rules inline: ```dart final passwordSchema = Validasi.string([ StringRules.minLength(8), InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Password must contain an uppercase letter'; } return null; // null means valid }), InlineRule((value) { if (!value.contains(RegExp(r'[0-9]'))) { return 'Password must contain a number'; } return null; }), ]); final result = passwordSchema.validate('MyPass123'); print('Password is valid: ${result.isValid}'); ``` ## Putting It All Together Here's a complete example validating a registration form: ```dart final registrationSchema = Validasi.map([ MapRules.hasFields({ 'username': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(3), StringRules.maxLength(20), ]), 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.email(), ]), 'password': Validasi.string([ StringRules.minLength(8), InlineRule((value) { final hasUpper = value.contains(RegExp(r'[A-Z]')); final hasLower = value.contains(RegExp(r'[a-z]')); final hasDigit = value.contains(RegExp(r'[0-9]')); if (!hasUpper || !hasLower || !hasDigit) { return 'Password must contain uppercase, lowercase, and numbers'; } return null; }), ]), 'age': Validasi.number([ Nullable(), NumberRules.moreThanEqual(13), ]), 'terms': Validasi.any([ InlineRule((value) { return value == true ? null : 'You must accept the terms'; }), ]), }), ]); final formData = { 'username': ' JohnDoe ', 'email': 'JOHN@EXAMPLE.COM', 'password': 'SecurePass123', 'age': 25, 'terms': true, }; final result = registrationSchema.validate(formData); if (result.isValid) { print('✓ Registration successful!'); print('Processed data: ${result.data}'); } else { print('✗ Validation failed:'); for (var error in result.errors) { print(' - ${error.path?.join('.')}: ${error.message}'); } } ``` ## Next Steps Now that you understand the basics, explore more advanced features: * [Validation Schemas](/guide/schemas) - Deep dive into schema creation * [Built-in Rules](/guide/rules) - Complete list of available rules * [Transformations](/guide/transformations) - Data preprocessing and transformation * [Error Handling](/guide/error-handling) - Working with validation errors --- --- url: /validasi/v0/quick-start.md --- # Quick Start This is a quick start guide to get you up and running with Validasi. ## Installation Simply run this command: ```bash pub add validasi ``` ## Usage Here's a simple example to get you started: ::: code-group ```dart [flutter_example.dart] import 'package:validasi/validasi.dart'; class MyForm extends StatelessWidget { @override Widget build(BuildContext context) { // In flutter, validation are done with helpers like `FieldValidator` // and `GroupValidator`. Both of these helpers return a `String?` value // which is the expected type for `validator` parameter // GroupValidator to specify validator for each field and use `using` method // to get the validator for each field final validator = GroupValidator({ 'name': Validasi.string() .minLength(1, message: 'name is required') .maxLength(255), 'email': Validasi.string() .minLength(1, message: 'email is required') .maxLength(255) .email(), }); return Form( child: Column( children: [ TextFormField( validator: validator.using('name').validate, autoValidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: 'Name', ), ), SizedBox(height: 10), TextFormField( validator: validator.using('email').validate, autoValidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: 'Email', ), ), SizedBox(height: 10), TextFormField( // inline validation validator: FieldValidator(Validasi.string().minLength(1, message: 'Inline example')).validate, autoValidateMode: AutovalidateMode.onUserInteraction, decoration: InputDecoration( labelText: 'Inline Example', ), ) ], ), ); } } ``` ```dart [dart_example.dart] import 'package:validasi/validasi.dart'; void main() { // Create object validation, it different from GroupValidator // because it will validate the map and return the result final validator = Validasi.object({ 'name': Validasi.string().minLength(1, message: 'name is required').maxLength(255), 'email': Validasi.string().minLength(1, message: 'email is required').maxLength(255).email(), }); // tryParse is used to validate the input value // and return the result final result = validator.tryParse({ 'name': 'John Doe', 'email': 'johndoe@example.com', }); // without using FieldValidator/GroupValidator, the result will be more // verbose. if (result.isValid) { print('Validation success'); } else { print('Validation failed'); print(result.errors); // List } } ``` ::: See [Basic Concept](/v0/guide/basic-concept) for more information about Validasi specific usage. And see [API Reference](https://pub.dev/documentation/validasi/latest) for more details on the available methods and options. --- --- url: /validasi/v0/guide/execution-order.md --- # Rules Execution Order In Validasi the Rule Execution order is as follow: ![Validasi Execution Order](/assets/validasi_execution_order.D4zLSojB.png) Notice where some flow goes to **END** directly. * The first one is when the value is `null`. If the `nullable` is set, the flow will jump and skip right to the custom rule execution. * When the value is not null, The validator will check the type of the value and if it's not the same as the schema, the validator will check for the transformer, if it is set the transformer will be executed to transform the value to the desired type. If the transformation failed or no transformer is set, the validaton will end and return the error. * The last on is the required rule, this flow can only occur when `nullable` is not set. The required rule simply check if the value is not `null`. If the value is `null` the validation will end and return error. * The built-in rules will be executed in the order they are defined. Each of the rules will be executed even if the previous built-in rule failed. The custom rule will be executed after all the built-in rules are all executed. --- --- url: /validasi/v0/types/string.md --- # String The `String` schema is used to validate the input value as a string. This schema contains some useful built-in validators to validate the input value. The following code shows how to create a `String` schema: ::: code-group ```dart [Using Validasi] Validasi.string(); ``` ```dart [Using Direct Class] StringValidator(); ``` ::: Below are the available methods for the `String` schema: \[\[toc]] ## minLength ```dart Validasi.string().minLength(int length, {String? message}); ``` The `minLength` method is used to validate the minimum length of the input value. This method will return an error message if the input value is less than the specified length. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().minLength(3); final result = schema.tryParse('hi'); print(result.errors.first.message); // 'field must contains at least 3 characters' } ``` ## maxLength ```dart Validasi.string().maxLength(int length, {String? message}); ``` The `maxLength` method is used to validate the maximum length of the input value. This method will return an error message if the input value is more than the specified length. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().maxLength(3); final result = schema.tryParse('hello'); print(result.errors.first.message); // 'field must not be longer than 3 characters' } ``` ## email ```dart Validasi.string().email({bool allowTopLevelDomain = false, bool allowInternational = false,String? message}); ``` The `email` method is used to validate the input value as an email. This method will return an error message if the input value is not a valid email. By default, the `allowTopLevelDomain` and `allowInternational` parameters are set to `false`. Setting `allowTopLevelDomain` to `true` will allow the email to have a top-level domain (e.g., `.com`, `.net`, `.org`) to be omitted. Setting `allowInternational` to `true` will allow the email to have international characters. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().email(); final result = schema.tryParse('hello@world'); print(result.errors.first.message); // 'field must be a valid email' } ``` ## startsWith ```dart Validasi.string().startsWith(String text, {String? message}); ``` The `startsWith` method is used to validate the input value to start with the specified text. This method will return an error message if the input value does not start with the specified text. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().startsWith('hello'); final result = schema.tryParse('world'); print(result.errors.first.message); // 'field must start with "hello"' } ``` ## endsWith ```dart Validasi.string().endsWith(String text, {String? message}); ``` The `endsWith` method is used to validate the input value to end with the specified text. This method will return an error message if the input value does not end with the specified text. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().endsWith('world'); final result = schema.tryParse('hello'); print(result.errors.first.message); // 'field must end with "world"' } ``` ## contains ```dart Validasi.string().contains(String text, {String? message}); ``` The `contains` method is used to validate the input value to contain the specified text. This method will return an error message if the input value does not contain the specified text. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().contains('world'); final result = schema.tryParse('hello'); print(result.errors.first.message); // 'field must contains "world"' } ``` ## url ```dart Validasi.string().url({String? message, List checks = const [UrlChecks.scheme, UrlChecks.host]}); ``` The `url` method is used to validate the input value as a URL. This method will return an error message if the input value is not a valid URL. Additionally, you can specify the `checks` parameter to validate the URL with specific checks. The available checks are: * `UrlChecks.scheme`: Check if the URL has a valid scheme (e.g., `http`, `https`). * `UrlChecks.host`: Check if the URL has a valid host (e.g., `example.com`). * `UrlChecks.httpsOnly`: Check if the URL is using HTTPS scheme. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().url( checks: [...defaultUrlChecks, UrlChecks.httpsOnly] ); final result = schema.tryParse('hello'); print(result.errors.first.message); // 'field must be a valid URL' } ``` ## regex ```dart Validasi.string().regex(String pattern, {String? message}); ``` The `regex` method is used to validate the input value using a regular expression pattern. This method will return an error message if the input value does not match the specified pattern. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.string().regex(r'^[0-9]+$'); final result = schema.tryParse('hello'); print(result.errors.first.message); // 'field must match the pattern' } ``` --- --- url: /validasi/rules/string.md --- # String Rules String validation rules for text input validation. ## StringRules.minLength() Validates that a string has a minimum length. ### Syntax ```dart StringRules.minLength(int length, {String? message}) ``` ### Parameters * **length** (int, required) - The minimum number of characters required * **message** (String?, optional) - Custom error message ### Default Error Message ``` "Minimum length is {length} characters" ``` ### Example ```dart final usernameSchema = Validasi.string([ StringRules.minLength(3, message: 'Username must be at least 3 characters'), ]); print(usernameSchema.validate('ab').isValid); // false print(usernameSchema.validate('abc').isValid); // true ``` ## StringRules.maxLength() Validates that a string does not exceed a maximum length. ### Syntax ```dart StringRules.maxLength(int length, {String? message}) ``` ### Parameters * **length** (int, required) - The maximum number of characters allowed * **message** (String?, optional) - Custom error message ### Default Error Message ``` "Maximum length is {length} characters" ``` ### Example ```dart final bioSchema = Validasi.string([ StringRules.maxLength(500, message: 'Bio cannot exceed 500 characters'), ]); print(bioSchema.validate('Hello').isValid); // true print(bioSchema.validate('a' * 501).isValid); // false ``` ## StringRules.oneOf() Validates that a string matches one of the allowed values (whitelist validation). ### Syntax ```dart StringRules.oneOf(List validValues, {String? message}) ``` ### Parameters * **`validValues`** (`List`, required) - List of allowed string values * **`message`** (String?, optional) - Custom error message ### Default Error Message ``` "Value must be one of: {validValues}" ``` ### Example ```dart final statusSchema = Validasi.string([ StringRules.oneOf( ['active', 'inactive', 'pending'], message: 'Invalid status', ), ]); print(statusSchema.validate('active').isValid); // true print(statusSchema.validate('deleted').isValid); // false ``` ## See Also * [String Validation Examples](/examples/string-validation) --- --- url: /validasi/examples/string-validation.md --- # String Validation Examples Learn how to validate strings with Validasi's powerful string validation rules. ## Basic Length Validation ### Minimum Length ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final schema = Validasi.string([ StringRules.minLength(3), ]); // Valid print(schema.validate('abc').isValid); // true print(schema.validate('hello').isValid); // true // Invalid print(schema.validate('ab').isValid); // false ``` ### Maximum Length ```dart final schema = Validasi.string([ StringRules.maxLength(10), ]); // Valid print(schema.validate('short').isValid); // true // Invalid print(schema.validate('this is too long').isValid); // false ``` ### Combined Length Rules ```dart final usernameSchema = Validasi.string([ StringRules.minLength(3), StringRules.maxLength(20), ]); final result = usernameSchema.validate('john_doe'); print('Valid username: ${result.isValid}'); ``` ## Pattern Matching ### Email Validation ```dart final emailSchema = Validasi.string([ StringRules.email(), ]); // Valid emails print(emailSchema.validate('user@example.com').isValid); // true print(emailSchema.validate('john.doe@company.co.uk').isValid); // true // Invalid emails print(emailSchema.validate('invalid.email').isValid); // false print(emailSchema.validate('@example.com').isValid); // false ``` ### URL Validation ```dart final urlSchema = Validasi.string([ StringRules.url(), ]); // Valid URLs print(urlSchema.validate('https://example.com').isValid); // true print(urlSchema.validate('http://localhost:3000').isValid); // true // Invalid URLs print(urlSchema.validate('not a url').isValid); // false ``` ## Enum Validation Validate that a string is one of specific values: ```dart final statusSchema = Validasi.string([ StringRules.oneOf(['active', 'inactive', 'pending']), ]); // Valid print(statusSchema.validate('active').isValid); // true print(statusSchema.validate('pending').isValid); // true // Invalid print(statusSchema.validate('unknown').isValid); // false ``` ## String Transformation ### Trimming Whitespace ```dart final trimmedSchema = Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(3), ]); final result = trimmedSchema.validate(' hello '); print('Trimmed: "${result.data}"'); // Output: "hello" ``` ### Converting to Lowercase ```dart final lowercaseSchema = Validasi.string([ Transform((value) => value?.toLowerCase()), ]); final result = lowercaseSchema.validate('HELLO WORLD'); print('Lowercase: ${result.data}'); // Output: "hello world" ``` ### Chaining Transformations ```dart final normalizedSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), StringRules.minLength(3), ]); final result = normalizedSchema.validate(' HELLO '); print('Normalized: "${result.data}"'); // Output: "hello" ``` ## Custom String Rules Create custom validation for specific patterns: ```dart // Phone number validation final phoneSchema = Validasi.string([ InlineRule((value) { final phoneRegex = RegExp(r'^\+?[\d\s-()]+$'); if (!phoneRegex.hasMatch(value)) { return 'Invalid phone number format'; } return null; }), ]); // Alphanumeric validation final alphanumericSchema = Validasi.string([ InlineRule((value) { final alphanumericRegex = RegExp(r'^[a-zA-Z0-9]+$'); if (!alphanumericRegex.hasMatch(value)) { return 'Must contain only letters and numbers'; } return null; }), ]); ``` ## Real-World Examples ### Username Validation ```dart final usernameSchema = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(3), StringRules.maxLength(20), InlineRule((value) { final usernameRegex = RegExp(r'^[a-z0-9_]+$'); if (!usernameRegex.hasMatch(value)) { return 'Username can only contain lowercase letters, numbers, and underscores'; } return null; }), ]); print(usernameSchema.validate('John_Doe123').isValid); // true (after transform) print(usernameSchema.validate('invalid@user').isValid); // false ``` ### Password Strength Validation ```dart final strongPasswordSchema = Validasi.string([ StringRules.minLength(8), InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain at least one uppercase letter'; } return null; }), InlineRule((value) { if (!value.contains(RegExp(r'[a-z]'))) { return 'Must contain at least one lowercase letter'; } return null; }), InlineRule((value) { if (!value.contains(RegExp(r'[0-9]'))) { return 'Must contain at least one number'; } return null; }), InlineRule((value) { if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) { return 'Must contain at least one special character'; } return null; }), ]); final result = strongPasswordSchema.validate('MyP@ssw0rd'); print('Strong password: ${result.isValid}'); ``` ### Slug Validation ```dart final slugSchema = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), Transform((value) => value?.replaceAll(RegExp(r'\s+'), '-')), StringRules.minLength(3), InlineRule((value) { final slugRegex = RegExp(r'^[a-z0-9-]+$'); if (!slugRegex.hasMatch(value)) { return 'Invalid slug format'; } return null; }), ]); final result = slugSchema.validate('Hello World Post'); print('Slug: ${result.data}'); // Output: "hello-world-post" ``` ## Next Steps * [Number Validation Examples](/rules/number) * [Complex Structures](/examples/complex-structures) * [Built-in Rules Reference](/guide/rules) --- --- url: /validasi/guide/transformations.md --- # Transformations Transformations allow you to modify and preprocess data during validation. This powerful feature enables you to clean, normalize, and convert data before applying validation rules. ## Basic Transformations ### Transform Rule The `Transform` rule modifies values as they pass through validation: ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final schema = Validasi.string([ Transform((value) => value?.trim()), StringRules.minLength(3), ]); final result = schema.validate(' hello '); print(result.data); // "hello" (trimmed) ``` The transformed value is: * Used for subsequent validation rules * Returned in `result.data` if validation succeeds ## Common String Transformations ### Trimming Whitespace ```dart final trimSchema = Validasi.string([ Transform((value) => value?.trim()), ]); print(trimSchema.validate(' text ').data); // "text" ``` ### Converting Case ```dart // Lowercase final lowercaseSchema = Validasi.string([ Transform((value) => value?.toLowerCase()), ]); print(lowercaseSchema.validate('HELLO').data); // "hello" // Uppercase final uppercaseSchema = Validasi.string([ Transform((value) => value?.toUpperCase()), ]); print(uppercaseSchema.validate('hello').data); // "HELLO" ``` ### Normalizing Input ```dart final normalizedEmailSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), StringRules.minLength(5), ]); final result = normalizedEmailSchema.validate(' USER@EXAMPLE.COM '); print(result.data); // "user@example.com" ``` ### Replacing Characters ```dart final slugSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), Transform((value) => value?.replaceAll(RegExp(r'\s+'), '-')), Transform((value) => value?.replaceAll(RegExp(r'[^a-z0-9-]'), '')), ]); final result = slugSchema.validate('Hello World! 2024'); print(result.data); // "hello-world-2024" ``` ## Chaining Transformations Multiple `Transform` rules execute in order: ```dart final schema = Validasi.string([ Nullable(), Transform((value) => value?.trim()), // 1. Remove whitespace Transform((value) => value?.toLowerCase()), // 2. Convert to lowercase Transform((value) => value?.replaceAll(' ', '-')), // 3. Replace spaces StringRules.minLength(3), // 4. Then validate ]); final result = schema.validate(' Hello World '); print(result.data); // "hello-world" ``` ::: tip Order Matters Place `Transform` rules before validation rules to ensure the transformed value is validated, not the original. ::: ## Preprocessing with ValidasiTransformation For type conversion before validation, use `withPreprocess`: ```dart import 'package:validasi/transformer.dart'; final schema = Validasi.string([ StringRules.minLength(3), ]).withPreprocess( ValidasiTransformation((value) => value.toString()), ); // Converts number to string, then validates final result = schema.validate(12345); print(result.isValid); // true print(result.data); // "12345" ``` ### Type Conversion Examples **Number to String:** ```dart final numberToStringSchema = Validasi.string([ StringRules.minLength(1), ]).withPreprocess( ValidasiTransformation((value) => value.toString()), ); print(numberToStringSchema.validate(42).data); // "42" ``` **String to Number:** ```dart final stringToNumberSchema = Validasi.number([ NumberRules.moreThan(0), ]).withPreprocess( ValidasiTransformation((value) { if (value is String) { return int.tryParse(value) ?? 0; } return value; }), ); print(stringToNumberSchema.validate('42').data); // 42 ``` **JSON String to Map:** ```dart import 'dart:convert'; final jsonSchema = Validasi.map([ MapRules.hasFieldKeys({'name', 'age'}), ]).withPreprocess( ValidasiTransformation((value) { if (value is String) { return jsonDecode(value); } return value; }), ); final result = jsonSchema.validate('{"name":"John","age":30}'); print(result.data); // {name: John, age: 30} ``` ## Practical Transformation Examples ### Email Normalization ```dart final emailSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), StringRules.minLength(5), InlineRule((value) { if (!value.contains('@')) { return 'Invalid email format'; } return null; }), ]); final result = emailSchema.validate(' USER@EXAMPLE.COM '); print(result.data); // "user@example.com" ``` ### Phone Number Cleaning ```dart final phoneSchema = Validasi.string([ Transform((value) => value?.replaceAll(RegExp(r'[\s\-\(\)]'), '')), StringRules.minLength(10), InlineRule((value) { if (!RegExp(r'^\+?\d+$').hasMatch(value)) { return 'Invalid phone number'; } return null; }), ]); final result = phoneSchema.validate('(555) 123-4567'); print(result.data); // "5551234567" ``` ### Username Normalization ```dart final usernameSchema = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), Transform((value) => value?.replaceAll(RegExp(r'\s+'), '_')), StringRules.minLength(3), StringRules.maxLength(20), InlineRule((value) { if (!RegExp(r'^[a-z0-9_]+$').hasMatch(value)) { return 'Username can only contain letters, numbers, and underscores'; } return null; }), ]); final result = usernameSchema.validate(' John Doe '); print(result.data); // "john_doe" ``` ### Price Formatting ```dart final priceSchema = Validasi.number([ NumberRules.moreThan(0.0), ]).withPreprocess( ValidasiTransformation((value) { if (value is String) { // Remove currency symbols and parse final cleaned = value.replaceAll(RegExp(r'[^\d.]'), ''); return double.tryParse(cleaned) ?? 0.0; } return value; }), ); print(priceSchema.validate('$99.99').data); // 99.99 print(priceSchema.validate('€149.50').data); // 149.5 ``` ### Date String to DateTime ```dart final dateSchema = Validasi.any([ InlineRule((value) { if (value.isAfter(DateTime.now())) { return 'Date cannot be in the future'; } return null; }), ]).withPreprocess( ValidasiTransformation((value) { if (value is String) { return DateTime.tryParse(value) ?? DateTime.now(); } return value; }), ); final result = dateSchema.validate('2024-01-15'); print(result.data); // DateTime object ``` ## Complex Transformations ### Nested Map Transformations ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'email': Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), ]), 'name': Validasi.string([ Transform((value) => value?.trim()), Transform((value) { // Capitalize first letter of each word return value?.split(' ') .map((word) => word.isEmpty ? '' : word[0].toUpperCase() + word.substring(1).toLowerCase()) .join(' '); }), ]), 'tags': Validasi.list([ IterableRules.forEach( Validasi.string([ Transform((value) => value?.trim().toLowerCase()), ]), ), ]), }), ]); final result = userSchema.validate({ 'email': ' USER@EXAMPLE.COM ', 'name': 'john doe', 'tags': [' Flutter ', ' DART '], }); print(result.data); // { // email: "user@example.com", // name: "John Doe", // tags: ["flutter", "dart"] // } ``` ### Conditional Transformations ```dart final schema = Validasi.string([ Transform((value) { if (value == null) return null; // Only trim if value starts/ends with whitespace if (value.startsWith(' ') || value.endsWith(' ')) { return value.trim(); } return value; }), StringRules.minLength(3), ]); ``` ## Transformation with Nullable When using `Nullable()`, handle null in transformations: ```dart final schema = Validasi.string([ Nullable(), Transform((value) => value?.trim()), // Safe navigation Transform((value) => value?.toLowerCase()), ]); print(schema.validate(null).isValid); // true print(schema.validate(null).data); // null print(schema.validate(' HELLO ').data); // "hello" ``` ## Performance Considerations ### Efficient Transformations ```dart // Good ✓ - Single transformation final efficient = Validasi.string([ Transform((value) { if (value == null) return null; return value.trim().toLowerCase().replaceAll(' ', '-'); }), ]); // Less efficient ✗ - Multiple transformations final lessEfficient = Validasi.string([ Transform((value) => value?.trim()), Transform((value) => value?.toLowerCase()), Transform((value) => value?.replaceAll(' ', '-')), ]); ``` However, multiple transformations are fine for readability: ```dart // More readable, slight performance cost final readable = Validasi.string([ Transform((value) => value?.trim()), // Clear intent Transform((value) => value?.toLowerCase()), // Clear intent Transform((value) => value?.replaceAll(' ', '-')), // Clear intent ]); ``` ## Best Practices ### 1. Order Your Rules ```dart final schema = Validasi.string([ Nullable(), // 1. Handle nulls first Transform((value) => value?.trim()), // 2. Clean data StringRules.minLength(3), // 3. Then validate ]); ``` ### 2. Use Safe Navigation ```dart // Good ✓ Transform((value) => value?.trim()) // May throw ✗ Transform((value) => value.trim()) ``` ### 3. Keep Transformations Pure Transformations should not have side effects: ```dart // Good ✓ Transform((value) => value?.toLowerCase()) // Avoid ✗ Transform((value) { saveToDatabase(value); // Side effect! return value; }) ``` ### 4. Document Complex Logic ```dart final schema = Validasi.string([ Transform((value) { // Normalize whitespace: trim and collapse multiple spaces return value?.trim().replaceAll(RegExp(r'\s+'), ' '); }), ]); ``` ## Next Steps * [Error Handling](/guide/error-handling) - Handle validation errors * [Examples](/examples/string-validation) - See more examples * [Advanced](/advanced/custom-rule) - Create custom rules --- --- url: /validasi/v0/guide/transformer.md --- # Transformer Transformer is a way to convert the input value to another type. This is useful when you want to convert the input value to another type before the validation process. You can also use the transformer to handle the error when the conversion failed ## Using Transformer You can use the `transformer` parameter to provide a transformer to the supported schema. ```dart import 'package:validasi/validasi.dart'; void main() { final schema = Validasi.number(transformer: NumberTransformer()); final result = schema.tryParse('10'); print(result.value); // 10 } ``` When the conversion failed, the transformer will return an error message (`invalidType`). ## Built-in Transformers Validasi provides a few built-in transformer: * [`NumberTransformer`](#numbertransformer) * [`StringTransformer`](#stringtransformer) * [`DateTransformer`](#datetransformer) ### NumberTransformer The `NumberTransformer` is used to convert the input value to a number. This transformer will convert the input value to a number using the `num.parse` method. ```dart import 'package:validasi/validasi.dart'; final schema = Validasi.number(transformer: NumberTransformer()); // [!code focus:2] print(schema.tryParse('10').value); // 10 ``` ### StringTransformer The `StringTransformer` is used to convert the input value to a string. This transformer will convert the input value to a string using the `toString` method. ```dart import 'package:validasi/validasi.dart'; final schema = Validasi.string(transformer: StringTransformer()); // [!code focus:2] print(schema.tryParse(10).value); // '10' ``` ### DateTransformer The `DateTransformer` is used to convert the input value to a date. This transformer will convert the input value to a date using the `DateFormat.tryParse` method. ```dart import 'package:validasi/validasi.dart'; final schema = Validasi.date(transformer: DateTransformer()); // [!code focus:2] print(schema.tryParse('2021-01-01').value); // DateTime(2021-01-01 00:00:00.000) ``` ## Building your own Transformer You can also build your own transformer by extending the `Transformer` class. ```dart import 'package:validasi/validasi.dart'; class MyTransformer extends Transformer { @override String? transform(value, fail) { return convert(value); } } ``` The `transform` method will receive the input value and the `fail` function. You can use the `fail` function to handle the error when the conversion failed. The `fail` function accept a message as the parameter. You can use this message as the error message when the conversion failed. When the conversion fail, the transformer will return an error message (`invalidType`). The transformer could also return `null`. It could fails depend whatever `nullable` is set or not. If not, then the required rule will run and return the error message (`required`). See [Execution Flow](/v0/guide/execution-order) for more information. --- --- url: /validasi/advanced/engine.md --- # Understanding the Validasi Engine The Validasi engine is the core component that orchestrates the validation process. This guide provides a deep dive into how the engine processes data, applies transformations, executes rules, and manages validation flow. ## Engine Architecture The `ValidasiEngine` is responsible for: 1. **Preprocessing**: Transforming raw input into the expected type 2. **Type checking**: Ensuring type safety 3. **Rule execution**: Running validation rules sequentially 4. **Context management**: Maintaining validation state 5. **Result generation**: Producing structured validation results 6. **Caching**: Optimizing repeated validations ## Validation Pipeline When you call `validate()`, the engine executes a multi-stage pipeline: ``` Input Value ↓ ┌───────────────┐ │ Cache Lookup │ ← Check if already validated └───────┬───────┘ ↓ (miss) ┌───────────────┐ │ Preprocess │ ← Transform raw input to type T └───────┬───────┘ ↓ ┌───────────────┐ │ Type Check │ ← Verify value is T or T? └───────┬───────┘ ↓ ┌───────────────┐ │ Rule Loop │ ← Execute each rule sequentially │ - runOnNull? │ │ - apply() │ │ - stopped? │ └───────┬───────┘ ↓ ┌───────────────┐ │ Build Result │ ← Create ValidasiResult └───────┬───────┘ ↓ ┌───────────────┐ │ Cache Store │ ← Store result for future lookups └───────┬───────┘ ↓ Return Result ``` ## Stage 1: Cache Lookup Before running validation, the engine checks if this input was validated before. ```dart final cacheKey = cacheEnabled ? computeCacheKey(originalInput) : null; if (cacheKey != null && cacheEnabled) { final cached = EngineCache.get(this, cacheKey); if (cached != null) { return cached as ValidasiResult; } } ``` **Key Points**: * Cache lookup happens with the **original input** before preprocessing * If cache hit, entire pipeline is skipped * Cache is enabled by default (`cacheEnabled: true`) **Example**: ```dart final schema = ValidasiEngine(rules: [MoreThan(0)]); // First call: full pipeline final result1 = schema.validate(5); // Second call: cache hit, immediate return final result2 = schema.validate(5); ``` ## Stage 2: Preprocessing Preprocessing transforms the raw input into the expected type `T`. This is useful for type conversion or data normalization. ```dart if (preprocess != null) { final result = preprocess!.tryTransform(value); if (!result.isValid) { return ValidasiResult.error( ValidationError( rule: 'Preprocess', message: 'Failed to preprocess value', details: { 'exception': result.error?.toString() ?? 'Unknown error', }, ), ); } value = result.data; } ``` **Key Points**: * Preprocessing runs **before** type checking * Preprocessing failures create a validation error with rule name `'Preprocess'` * Transformed value is used for subsequent stages **Example**: ```dart // Convert string to int before validation final schema = ValidasiEngine( preprocess: ValidasiTransformation.tryParse(int.parse), rules: [MoreThan(0)], ); // Input: "42" (String) // After preprocess: 42 (int) // Then: validate against rules final result = schema.validate("42"); print(result.isValid); // true print(result.data); // 42 ``` ### Adding Preprocessing Use `withPreprocess()` to add preprocessing to an existing engine: ```dart final baseEngine = ValidasiEngine( rules: [MoreThan(0), LessThan(100)], ); final engineWithPreprocess = baseEngine.withPreprocess( ValidasiTransformation.tryParse(int.parse), ); engineWithPreprocess.validate("50"); // Parses "50" to 50 ``` ## Stage 3: Type Checking After preprocessing, the engine verifies the value matches type `T`: ```dart if (value is! T?) { return ValidasiResult.error(ValidationError( rule: 'TypeCheck', message: 'Expected type $T, got ${value.runtimeType}', details: {'value': value}, )); } ``` **Key Points**: * Type check happens **after** preprocessing * The check is `T?` (nullable) to allow null values * Type errors create a validation error with rule name `'TypeCheck'` * Validation stops immediately on type mismatch **Example**: ```dart final schema = ValidasiEngine(rules: [MoreThan(0)]); // Type mismatch: expects int, got String final result = schema.validate("hello"); print(result.isValid); // false print(result.errors.first.rule); // 'TypeCheck' ``` ## Stage 4: Context Creation The engine creates a `ValidationContext` to manage validation state: ```dart final context = ValidationContext(value: value); ``` The context provides: * `value`: Current value being validated (mutable) * `errors`: List of validation errors * `isStopped`: Flag to stop rule execution * Methods: `setValue()`, `addError()`, `stop()`, `requireValue` **Example**: ```dart // Context is created internally, but rules interact with it: class CustomRule extends Rule { @override void apply(ValidationContext context) { // Access current value final value = context.value; // Add error context.addError(ValidationError( rule: 'CustomRule', message: 'Validation failed', )); // Transform value context.setValue(value?.toUpperCase()); // Stop further validation context.stop(); } } ``` ## Stage 5: Rule Loop The engine iterates through all rules and applies them sequentially: ```dart for (final rule in rules ?? >[]) { // Skip null values unless rule explicitly handles them if (context.value == null && !rule.runOnNull) { continue; } // Apply the rule rule.apply(context); // Stop if requested if (context.isStopped) { break; } } ``` ### Rule Execution Order Rules execute in the order they're defined: ```dart final schema = ValidasiEngine( rules: [ MinLength(5), // 1st: Check minimum length MaxLength(20), // 2nd: Check maximum length Transform((s) => s?.trim()), // 3rd: Transform MinLength(3), // 4th: Check trimmed length ], ); ``` **Order matters**! Transformations affect subsequent validations: ```dart final schema = ValidasiEngine( rules: [ MinLength(10), // Checks original length Transform((s) => s?.trim()), // Removes whitespace MaxLength(5), // Checks trimmed length ], ); // " hi " → original length 8 (fails MinLength) // But would pass after trim ``` ### The `runOnNull` Check Before applying each rule, the engine checks if the value is null: ```dart if (context.value == null && !rule.runOnNull) { continue; // Skip this rule } ``` **Rules with `runOnNull = true`**: * `Required`: Must check null to add error * `Nullable`: Allows null values * `Transform`: May transform null to non-null **Rules with `runOnNull = false`** (default): * All type-specific rules (string, number, list, map) * These assume a non-null value exists **Example**: ```dart final schema = ValidasiEngine( rules: [ Nullable(), // runOnNull = true → executes MinLength(5), // runOnNull = false → skipped if null ], ); // Value is null schema.validate(null); // Valid! MinLength skipped ``` ### Context Value Changes Rules can modify the value during validation: ```dart final schema = ValidasiEngine( rules: [ Transform((s) => s?.toUpperCase()), // Changes value MinLength(5), // Validates transformed value ], ); final result = schema.validate("hello"); print(result.data); // "HELLO" (transformed) ``` **Flow**: 1. Initial value: `"hello"` 2. After Transform: `"HELLO"` (context.value changed) 3. MinLength validates: `"HELLO"` (5 chars, passes) ### Stopping Validation Rules can stop the validation chain using `context.stop()`: ```dart class StopOnError extends Rule { @override void apply(ValidationContext context) { if (context.requireValue.isEmpty) { context.addError(ValidationError( rule: 'StopOnError', message: 'Empty value', )); context.stop(); // Prevents further rules from running } } } final schema = ValidasiEngine( rules: [ StopOnError(), // Stops if empty MinLength(5), // Won't execute if stopped MaxLength(20), // Won't execute if stopped ], ); schema.validate(""); // Only StopOnError executes ``` **When to stop**: * Critical validation failure (no point continuing) * Performance optimization (skip expensive checks) * Error cascades (prevent redundant errors) ## Stage 6: Result Generation After all rules execute (or stop), the engine creates the result: ```dart final ValidasiResult result = ValidasiResult( isValid: context.errors.isEmpty, data: context.value, errors: context.errors, ); ``` **Result properties**: * `isValid`: `true` if no errors were added * `data`: Final value (may be transformed by rules) * `errors`: List of all validation errors **Example**: ```dart final schema = ValidasiEngine( rules: [ Required(), MinLength(5), MaxLength(10), ], ); final result = schema.validate("hi"); print(result.isValid); // false print(result.data); // "hi" print(result.errors.length); // 1 (MinLength failed) print(result.errors.first.rule); // "MinLength" ``` ## Stage 7: Cache Storage If caching is enabled, the result is stored for future lookups: ```dart if (cacheKey != null && cacheEnabled) { EngineCache.set(this, cacheKey, result); } ``` **Key Points**: * Cache stores the **final result** (including transformed value and errors) * Cache key is computed from **original input** (before preprocessing) * Cache is per-engine-instance **Example**: ```dart final schema = ValidasiEngine( rules: [Transform((s) => s?.toUpperCase()), MinLength(5)], ); // First validation: full pipeline final result1 = schema.validate("hello"); print(result1.data); // "HELLO" // Second validation: cache hit final result2 = schema.validate("hello"); print(result2.data); // "HELLO" (from cache, transform not re-run) ``` ## Complete Example: Engine Flow Let's trace a complete validation through all stages: ```dart final schema = ValidasiEngine( preprocess: ValidasiTransformation.tryParse(int.parse), rules: [ Required(), MoreThan(0), LessThan(100), Transform((n) => n! * 2), LessThan(150), ], ); final result = schema.validate("42"); ``` **Execution trace**: 1. **Cache Lookup**: No cached result for `"42"` 2. **Preprocess**: * Input: `"42"` (String) * Transform: `int.parse("42")` * Output: `42` (int) 3. **Type Check**: * Check: `42 is int?` → ✓ true 4. **Context Creation**: * `context.value = 42` * `context.errors = []` 5. **Rule Loop**: * **Required**: value = 42 (not null) → passes * **MoreThan(0)**: 42 > 0 → ✓ passes * **LessThan(100)**: 42 < 100 → ✓ passes * **Transform**: 42 \* 2 = 84 → `context.value = 84` * **LessThan(150)**: 84 < 150 → ✓ passes 6. **Result Generation**: * `isValid = true` (no errors) * `data = 84` (transformed) * `errors = []` 7. **Cache Storage**: Store result for `"42"` 8. **Return**: `ValidasiResult(isValid: true, data: 84)` ## Advanced Patterns ### Conditional Rule Application Use `Having` to conditionally apply rules: ```dart final schema = ValidasiEngine( rules: [ Having( (value) => value?.startsWith('@') ?? false, [MinLength(5)], // Only if starts with '@' ), ], ); schema.validate("@hi"); // Fails MinLength schema.validate("hi"); // Passes (Having condition false) ``` ### Error Accumulation The engine accumulates all errors, not just the first: ```dart final schema = ValidasiEngine( rules: [ MinLength(5), MaxLength(2), // Changed to 2 InlineRule((s) => s?.contains('@') ?? false, message: 'Must contain @'), ], ); final result = schema.validate("hi"); // Multiple rules fail: print(result.errors.length); // 2 (MinLength and InlineRule) ``` ### Preprocessing for Normalization Use preprocessing to normalize data before validation: ```dart // Normalize whitespace before validation final schema = ValidasiEngine( preprocess: ValidasiTransformation((input) { if (input is String) { return input.trim().toLowerCase(); } throw FormatException('Expected string'); }), rules: [ MinLength(3), MaxLength(20), ], ); // " HELLO " → "hello" → validates as "hello" final result = schema.validate(" HELLO "); print(result.data); // "hello" ``` ### Early Termination for Performance Stop validation early to avoid expensive checks: ```dart final schema = ValidasiEngine( rules: [ InlineRule((s) { if (s == null || s.isEmpty) { return false; // Failed } return true; }, message: 'Required'), InlineRule((s) { // Expensive check only if previous passed return expensiveValidation(s); }), ], ); ``` Or use explicit stopping: ```dart class RequiredWithStop extends Rule { @override bool get runOnNull => true; @override void apply(ValidationContext context) { if (context.value == null || context.value!.isEmpty) { context.addError(ValidationError( rule: 'Required', message: 'Field is required', )); context.stop(); // Skip expensive rules } } } ``` --- --- url: /validasi/v0.md --- --- --- url: /validasi/advanced/cache.md --- # Validation Cache Validasi includes a built-in caching mechanism to optimize validation performance when the same values are validated repeatedly. This guide explains how the cache works and how to control it. ## Overview The Validasi engine automatically caches validation results for each unique input value. When you validate the same value multiple times with the same schema, the engine returns the cached result instead of re-running all validation rules, significantly improving performance in scenarios with repeated validations. ## How the Cache Works ### Cache Lookup Process When you call `validate()`, the engine follows this process: 1. **Compute Cache Key**: Generates a structural key from the input value 2. **Check Cache**: Looks up if this exact input was validated before 3. **Return or Compute**: * If cached result exists → return immediately * If no cache → run validation and store result ```dart final schema = ValidasiEngine( rules: [ Required(), MinLength(5), MaxLength(20), ], ); // First validation: runs all rules and caches result final result1 = schema.validate('hello'); // Second validation: returns cached result instantly final result2 = schema.validate('hello'); // Different value: runs validation and caches new result final result3 = schema.validate('world'); ``` ### Cache Scope The cache is **per-engine-instance**. Each `ValidasiEngine` maintains its own separate cache: ```dart final schema1 = ValidasiEngine(rules: [MinLength(5)]); final schema2 = ValidasiEngine(rules: [MinLength(10)]); schema1.validate('hello'); // Cached in schema1 schema2.validate('hello'); // Separate cache in schema2 ``` ## Cache Key Generation Validasi uses a **structural caching strategy** that generates stable keys based on the content of the value, not its identity: ### Supported Types | Type | Cache Key Strategy | Example | |------|-------------------|---------| | `null` | Constant string | `'null'` | | `bool` | Boolean value | `'b:1'` or `'b:0'` | | `num` | Number with prefix | `'n:42'`, `'n:3.14'` | | `String` | Length + content | `'s:5:hello'` | | `List` | Length + recursive items | `'l[3]:n:1\|n:2\|n:3'` | | `Map` | Sorted entries | `'m{k1=v1\|k2=v2}'` | | `Object` | Identity + type | `'o:CustomType#12345'` | ### Examples ```dart // Strings: same content = same key schema.validate('hello'); // Key: 's:5:hello' schema.validate('hello'); // Same key → cache hit! // Numbers: same value = same key schema.validate(42); // Key: 'n:42' schema.validate(42.0); // Key: 'n:42.0' (different!) // Lists: structural equality schema.validate([1, 2, 3]); // Key: 'l[3]:n:1|n:2|n:3' schema.validate([1, 2, 3]); // Same key → cache hit! // Maps: keys are sorted for consistency schema.validate({'b': 2, 'a': 1}); // Key: 'm{a=n:1|b=n:2}' schema.validate({'a': 1, 'b': 2}); // Same key → cache hit! // Objects: identity-based (no structural caching) final obj1 = MyClass(); schema.validate(obj1); // Key: 'o:MyClass#67890' schema.validate(obj1); // Same instance → cache hit! final obj2 = MyClass(); schema.validate(obj2); // Different key → cache miss! ``` ## Cache Policy: LRU (Least Recently Used) The cache uses an **LRU eviction policy** with a fixed maximum size of **512 entries** per engine instance. ### How LRU Works 1. When a cached result is accessed, it becomes the "most recently used" 2. When the cache exceeds 512 entries, the **least recently used** entry is evicted 3. This ensures frequently validated values stay cached while old entries are removed ```dart final schema = ValidasiEngine(rules: [MinLength(1)]); // Add 512 entries for (var i = 0; i < 512; i++) { schema.validate('value$i'); } // Cache is full (512 entries) // Access an old entry → moves to front schema.validate('value0'); // Add new entry → evicts least recently used schema.validate('new_value'); // 'value1' is evicted ``` ### Why 512 Entries? The 512-entry limit balances memory usage and cache effectiveness: * **Small enough**: Prevents unbounded memory growth * **Large enough**: Handles most application validation patterns * **LRU policy**: Keeps frequently used validations cached ## Enabling and Disabling Cache Control caching when creating a schema: ```dart // Cache enabled (default) final engine = ValidasiEngine( rules: [MinLength(5)], cacheEnabled: true, ); // Cache disabled final engine = ValidasiEngine( rules: [MinLength(5)], cacheEnabled: false, ); // or from Validasi Validasi.withoutCache(() { return Validasi.string([StringRules.minLength(5)]).validate('test'); }) ``` ### Clearing the Cache Clear cached results for a specific schema: ```dart final schema = ValidasiEngine(rules: [MinLength(5)]); schema.validate('hello'); // Cached schema.validate('hello'); // Cache hit // Clear all cached results for this schema schema.clearCache(); schema.validate('hello'); // Cache miss → re-validates ``` ## When to Disable Cache While caching improves performance in most scenarios, consider disabling it when: ### 1. Validating Unique Values If every validation uses a different value, caching provides no benefit: ```dart // Bad use case for cache: every value is unique final schema = ValidasiEngine( rules: [MinLength(5)], cacheEnabled: false, // Disable to save memory ); for (var i = 0; i < 10000; i++) { schema.validate('unique_value_$i'); // Never repeats } ``` ### 2. Memory-Constrained Environments In embedded systems or memory-sensitive applications: ```dart final schema = ValidasiEngine( rules: [MinLength(5)], cacheEnabled: false, // Save memory ); ``` ### 3. Non-Deterministic Validation If validation depends on external state (time, random values, etc.), disable caching: ```dart final schema = ValidasiEngine( rules: [ InlineRule((value) { // Validation depends on current time return DateTime.now().hour >= 9 && DateTime.now().hour <= 17; }, message: 'Only available during business hours'), ], cacheEnabled: false, // Results change over time ); ``` ### 4. Rules with Side Effects If rules modify external state or have side effects: ```dart var validationCount = 0; final schema = ValidasiEngine( rules: [ InlineRule((value) { validationCount++; // Side effect return value != null; }), ], cacheEnabled: false, // Ensure rule runs every time ); ``` ## When to Use Cache (Default) Keep caching enabled (default) in these common scenarios: ### 1. Form Validation User input often repeats during typing: ```dart final emailSchema = ValidasiEngine( rules: [Required(), Email()], // cacheEnabled: true (default) ); // User types: "j" → "jo" → "joh" → "john" → "john@" → "john@ex" ... // Many intermediate states will be validated multiple times ``` ### 2. Repeated Validation When validating the same data multiple times: ```dart final schema = ValidasiEngine>( rules: [HasFields(['name', 'email'])], ); final userData = {'name': 'John', 'email': 'john@example.com'}; // Validate on form load schema.validate(userData); // Validate before submission schema.validate(userData); // Cache hit! // Validate after confirmation schema.validate(userData); // Cache hit! ``` ### 3. Batch Processing with Duplicates Processing lists where values may repeat: ```dart final schema = ValidasiEngine(rules: [Email()]); final emails = [ 'john@example.com', 'jane@example.com', 'john@example.com', // Duplicate 'bob@example.com', 'john@example.com', // Duplicate ]; for (final email in emails) { schema.validate(email); // Duplicates use cache } ``` ### 4. API Request Validation Validating similar API payloads: ```dart final requestSchema = ValidasiEngine>( rules: [ HasFields(['action', 'timestamp']), ConditionalField('action', (value) => value == 'update', 'data'), ], ); // Similar requests benefit from caching requestSchema.validate({'action': 'read', 'timestamp': '...'}); requestSchema.validate({'action': 'read', 'timestamp': '...'}); // Cache hit! ``` ## Performance Considerations ### Cache Hit Performance Cache hits are **extremely fast** - just a hash map lookup: ```dart final schema = ValidasiEngine( rules: [MinLength(5), MaxLength(100), Email()], ); // First validation: ~100 microseconds (depends on rules) final result1 = schema.validate('test@example.com'); // Cache hit: ~1 microsecond (just lookup) final result2 = schema.validate('test@example.com'); ``` ### Memory Usage Each cached entry stores: * Cache key (string) * `ValidasiResult` object Typical memory per entry: **100-500 bytes** depending on error count and details. Maximum memory per schema: **512 entries × ~300 bytes = ~150 KB** ### Cache Efficiency Monitor cache effectiveness in performance-critical applications: ```dart var cacheHits = 0; var cacheMisses = 0; final schema = ValidasiEngine(rules: [MinLength(5)]); for (final value in values) { final startTime = DateTime.now(); schema.validate(value); final duration = DateTime.now().difference(startTime); if (duration.inMicroseconds < 10) { cacheHits++; } else { cacheMisses++; } } print('Cache hit rate: ${(cacheHits / (cacheHits + cacheMisses) * 100).toStringAsFixed(1)}%'); ``` ## Best Practices ### ✅ Do * **Keep cache enabled by default** for most use cases * **Clear cache** when validation rules change dynamically * **Use cache** for form validation and repeated checks * **Monitor memory** in long-running applications with many schemas ### ❌ Don't * **Don't disable cache** unless you have a specific reason * **Don't clear cache** unnecessarily (defeats the purpose) * **Don't rely on cache** for rules with side effects * **Don't cache** when validating unique values --- --- url: /validasi/guide/schemas.md --- # Validation Schemas Schemas are the foundation of Validasi. They define the structure and rules for validating your data. This guide covers all schema types and how to use them effectively. ## Schema Types Validasi provides five main schema types, each optimized for specific data types: ### String Schema Validate string values with `Validasi.string()`: ```dart import 'package:validasi/validasi.dart'; import 'package:validasi/rules.dart'; final nameSchema = Validasi.string([ StringRules.minLength(2), StringRules.maxLength(50), ]); final result = nameSchema.validate('John Doe'); print(result.isValid); // true print(result.data); // "John Doe" ``` **Common Use Cases:** * User names and usernames * Email addresses * Passwords * Text input validation * URL and pattern validation ### Number Schema Validate numeric values with `Validasi.number()`: ```dart final ageSchema = Validasi.number([ NumberRules.moreThanEqual(0), NumberRules.lessThan(150), ]); final priceSchema = Validasi.number([ NumberRules.moreThan(0.0), NumberRules.finite(), // Ensures not infinity or NaN ]); print(ageSchema.validate(25).isValid); // true print(priceSchema.validate(99.99).isValid); // true ``` **Supported Number Types:** * `int` - Integer values * `double` - Floating-point values * `num` - Any numeric value ### List Schema Validate lists and iterables with `Validasi.list()`: ```dart final tagsSchema = Validasi.list([ IterableRules.minLength(1), IterableRules.forEach( Validasi.string([ StringRules.minLength(2), StringRules.maxLength(20), ]), ), ]); final result = tagsSchema.validate(['flutter', 'dart', 'mobile']); print(result.isValid); // true ``` **Nested List Validation:** ```dart // Validate list of lists final matrixSchema = Validasi.list>([ IterableRules.forEach( Validasi.list([ IterableRules.forEach( Validasi.number([ NumberRules.moreThanEqual(0), ]), ), ]), ), ]); final matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ]; print(matrixSchema.validate(matrix).isValid); // true ``` ### Map Schema Validate maps and objects with `Validasi.map()`: ```dart final userSchema = Validasi.map([ MapRules.hasFields({ 'name': Validasi.string([ StringRules.minLength(1), ]), 'email': Validasi.string([ StringRules.minLength(1), ]), 'age': Validasi.number([ NumberRules.moreThanEqual(18), ]), }), ]); final userData = { 'name': 'Alice', 'email': 'alice@example.com', 'age': 25, }; final result = userSchema.validate(userData); print(result.isValid); // true ``` **Nested Map Validation:** ```dart final addressSchema = Validasi.map([ MapRules.hasFields({ 'street': Validasi.string([ StringRules.minLength(1), ]), 'city': Validasi.string([ StringRules.minLength(1), ]), 'country': Validasi.string([ StringRules.minLength(1), ]), 'coordinates': Validasi.map([ MapRules.hasFields({ 'lat': Validasi.number([ NumberRules.moreThanEqual(-90.0), NumberRules.lessThanEqual(90.0), ]), 'lng': Validasi.number([ NumberRules.moreThanEqual(-180.0), NumberRules.lessThanEqual(180.0), ]), }), ]), }), ]); ``` ### Any Schema Validate any type with `Validasi.any()`: ```dart // Boolean validation final termsSchema = Validasi.any([ InlineRule((value) { return value == true ? null : 'You must accept the terms'; }), ]); // Custom type validation final customSchema = Validasi.any([ InlineRule((value) { if (value.isAfter(DateTime.now())) { return 'Date cannot be in the future'; } return null; }), ]); ``` ## Schema Composition Schemas can be composed and reused to build complex validations: ```dart // Reusable email schema final emailSchema = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), InlineRule((value) { if (!value.contains('@')) { return 'Invalid email format'; } return null; }), ]); // Reusable password schema final passwordSchema = Validasi.string([ StringRules.minLength(8), InlineRule((value) { if (!value.contains(RegExp(r'[A-Z]'))) { return 'Must contain uppercase letter'; } return null; }), ]); // Compose into registration schema final registrationSchema = Validasi.map([ MapRules.hasFields({ 'email': emailSchema, 'password': passwordSchema, 'confirmPassword': passwordSchema, }), // Add custom validation InlineRule>((value) { if (value['password'] != value['confirmPassword']) { return 'Passwords do not match'; } return null; }), ]); ``` ## Validation Results Every schema validation returns a `ValidasiResult` object: ```dart final result = schema.validate(data); // Check if valid if (result.isValid) { print('Valid! Data: ${result.data}'); } else { print('Invalid! Errors: ${result.errors}'); } ``` ### ValidasiResult Properties * **`isValid`** - `bool`: Whether validation passed * **`data`** - `T`: The validated (possibly transformed) data * **`errors`** - `List`: List of validation errors ### ValidasiError Properties * **`message`** - `String`: The error message * **`path`** - `List?`: Path to the field that failed (for nested structures) **Example with nested errors:** ```dart final result = userSchema.validate({ 'profile': { 'name': '', // Too short 'age': -5, // Invalid } }); for (var error in result.errors) { print('Error at ${error.path?.join('.')}: ${error.message}'); } // Output: // Error at profile.name: String must be at least 1 characters long // Error at profile.age: Number must be more than or equal to 0 ``` ## Best Practices ### 1. Create Reusable Schemas ```dart // Define once class Schemas { static final email = Validasi.string([ Transform((value) => value?.trim().toLowerCase()), StringRules.minLength(5), ]); static final positiveInt = Validasi.number([ NumberRules.moreThan(0), ]); } // Use everywhere final userSchema = Validasi.map([ MapRules.hasFields({ 'email': Schemas.email, 'age': Schemas.positiveInt, }), ]); ``` ### 2. Use Type Parameters Always specify type parameters for type safety: ```dart // Good ✓ final intList = Validasi.list([...]); final userData = Validasi.map([...]); // Avoid ✗ final list = Validasi.list([...]); // Less type-safe ``` ### 3. Order Rules Logically Place transformation rules before validation rules: ```dart final schema = Validasi.string([ Nullable(), // 1. Handle nulls first Transform(...), // 2. Transform data StringRules.minLength(3), // 3. Then validate ]); ``` ### 4. Provide Custom Error Messages Make errors user-friendly: ```dart final schema = Validasi.string([ StringRules.minLength(8, message: 'Password must be at least 8 characters'), StringRules.maxLength(128, message: 'Password is too long (max 128 characters)'), ]); ``` ## Next Steps * [Built-in Rules](/guide/rules) - Explore all available validation rules * [Transformations](/guide/transformations) - Learn about data transformation * [Error Handling](/guide/error-handling) - Handle validation errors effectively