Skip to main content

Getting Started

To start using the validation DSL, you first need to create a validator:
import xyz.mahmoudahmed.dsl.validation.*

// Create a pre-validation validator
val preValidator = ValidationDsl.preValidate {
    // Validation rules go here
}

// Create an in-validation validator
val inValidator = ValidationDsl.inValidate {
    // Validation rules go here
}

// Create a post-validation validator
val postValidator = ValidationDsl.postValidate {
    // Validation rules go here
}

Basic Field Validation

The most common validation is checking individual fields:
val userValidator = ValidationDsl.preValidate {
    // Required field (must not be null)
    validate("username")
        .required()
        .end()
        
    // String length validation
    validate("name")
        .required()
        .minLength(2)
        .maxLength(100)
        .end()
        
    // Pattern matching with regex
    validate("zipCode")
        .pattern(Regex("^\\d{5}(-\\d{4})?$"))
        .end()
        
    // Numeric value validation
    validate("age")
        .min(18)
        .max(120)
        .end()
        
    // Email validation
    validate("email")
        .required()
        .email()
        .end()
        
    // Date validation
    validate("birthDate")
        .date("yyyy-MM-dd")
        .end()
}

Complex Validation Rules

You can create more complex validation rules:
val orderValidator = ValidationDsl.preValidate {
    // Custom validation with a predicate
    validate("orderCode")
        .custom(
            predicate = { it?.toString()?.startsWith("ORD-") == true },
            message = "Order code must start with 'ORD-'"
        )
        .end()
        
    // Allowed values (enum-like validation)
    validate("status")
        .allowedValues("NEW", "PROCESSING", "SHIPPED", "DELIVERED", "CANCELLED")
        .end()
        
    // Validate related fields as a group
    validateGroup(
        "subtotal", 
        "tax", 
        "total",
        message = "Total must equal subtotal plus tax",
        code = "TOTAL_MISMATCH"
    ) { values ->
        val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
        val tax = (values[1] as? Number)?.toDouble() ?: 0.0
        val total = (values[2] as? Number)?.toDouble() ?: 0.0
        
        // Allow small rounding differences
        Math.abs((subtotal + tax) - total) < 0.01
    }
    
    // Field required only when another field has specific value
    validate("trackingNumber")
        .requiredIf("status", "SHIPPED", "DELIVERED")
        .end()
}

Conditional Validation

Apply validation rules only when certain conditions are met:
val profileValidator = ValidationDsl.preValidate {
    // Basic validation for everyone
    validate("name").required().end()
    validate("email").required().email().end()
    
    // Validate age only for specific countries
    validate("age")
        .required()
        .min(18)
        .`when` { data ->
            val country = (data as? Map<*, *>)?.get("country")?.toString()
            country == "US" || country == "CA"
        }
        .end()
    
    // Conditional block of validations
    `when`(condition = { data ->
        (data as? Map<*, *>)?.get("accountType")?.toString() == "BUSINESS"
    }) {
        validate("businessName").required().end()
        validate("taxId").required().pattern(Regex("^\\d{2}-\\d{7}$")).end()
        validate("industry").required().end()
    }
    
    // Validation with different severity level
    validate("phoneNumber")
        .pattern(Regex("^\\+?[0-9]{10,15}$"))
        .asWarning()  // This won't cause validation to fail
        .end()
}

Collection Validation

Validate collections and their items:
val orderValidator = ValidationDsl.preValidate {
    // Validate the collection itself
    validate("items")
        .required()
        .size(min = 1, max = 100)
        .end()
    
    // Validate each item in a collection
    forEach("items") {
        validate("productId")
            .required()
            .pattern(Regex("^PROD-\\d+$"))
            .end()
            
        validate("quantity")
            .required()
            .min(1)
            .end()
            
        validate("price")
            .required()
            .min(0.01)
            .end()
    }
    
    // Validate all fields matching a pattern
    validateAll("customer.preferences.*")
        .required()
        .end()
}

Validation Phases

PlatyMap supports three validation phases:
  1. Pre-Validation: Validates source data before mapping
    val preValidator = ValidationDsl.preValidate {
        // Validate source data
    }
    
  2. In-Validation: Validates during the mapping process
    val mapping = Platymap.flow("source")
        .to("target")
        .validateDuring {
            // Validate during mapping
        }
        .build()
    
  3. Post-Validation: Validates target data after mapping
    val postValidator = ValidationDsl.postValidate {
        // Validate target data
    }
    

Integration with Mapping

You can integrate validation with mapping in two ways:

Method 1: Separate Validation and Mapping

// Define validators
val preValidator = ValidationDsl.preValidate { /* validation rules */ }
val postValidator = ValidationDsl.postValidate { /* validation rules */ }

// Define mapping
val mapping = Platymap.flow("source")
    .to("target")
    .map("source.name").to("target.fullName")
    // More mapping rules...
    .build()

// Create validated mapping
val validatedMapping = createValidatedMapping(
    mapping,
    preValidator = preValidator,
    postValidator = postValidator,
    config = ValidationConfig(
        failFast = false,
        throwOnError = true,
        includeWarnings = true
    )
)

// Execute the validated mapping
try {
    val result = validatedMapping.executeToJson(sourceData)
    println("Mapping successful")
} catch (e: ValidationException) {
    println("Validation failed: ${e.message}")
    e.errors.forEach { error ->
        println("- ${error.path}: ${error.message}")
    }
}

Method 2: Fluent API Integration

// Define mapping with integrated validation
val mapping = Platymap.flow("source")
    .to("target")
    
    // Add pre-validation
    .withPreValidation {
        validate("source.id").required().end()
        validate("source.email").email().end()
    }
    
    // Define mapping rules
    .map("source.id").to("target.id")
    .map("source.email").to("target.contactEmail")
    
    // Add in-validation
    .validateDuring {
        validate("source.data").required().end()
    }
    
    // More mapping rules
    .map("source.data").to("target.info")
    
    // Add post-validation
    .withPostValidation {
        validate("target.id").required().end()
        validate("target.contactEmail").required().email().end()
    }
    
    // Build with validation
    .buildWithValidation()

// Execute the mapping
try {
    val result = mapping.executeToJson(sourceData)
    println("Mapping successful")
} catch (e: ValidationException) {
    println("Validation failed")
}

Error Handling

Validation produces ValidationResult objects that contain information about validation failures:
val validator = ValidationDsl.preValidate {
    // Validation rules...
}

// Validate data
val result = validator.validate(data)

if (result.isValid) {
    println("Validation passed!")
} else {
    println("Validation failed with ${result.errors.size} errors:")
    
    result.errors.forEach { error ->
        println("Field: ${error.path}")
        println("Message: ${error.message}")
        println("Value: ${error.value}")
        println("Code: ${error.code}")
        println("Severity: ${error.severity}")
        println()
    }
}
You can customize validation behavior with ValidationConfig:
val config = ValidationConfig(
    failFast = true,         // Stop at first error
    throwOnError = false,    // Don't throw exceptions
    includeWarnings = true,  // Include warnings in result
    includeInfos = false     // Exclude info messages
)

val result = validator.validate(data, config)

Best Practices

  1. Organize validators logically: Group related validations together
  2. Be specific with error messages: Provide clear guidance on how to fix issues
  3. Use appropriate severity levels: Not all issues are errors
  4. Validate at the right phase: Use pre-validation for input data, post-validation for output data
  5. Reuse validation logic: Extract common validation patterns to functions

Complete Examples

Customer Registration Validation

val registrationValidator = ValidationDsl.preValidate {
    // Personal information
    validate("customer.firstName")
        .required()
        .minLength(2)
        .maxLength(50)
        .end()
        
    validate("customer.lastName")
        .required()
        .minLength(2)
        .maxLength(50)
        .end()
        
    validate("customer.email")
        .required()
        .email()
        .end()
        
    validate("customer.password")
        .required()
        .minLength(8)
        .custom(
            predicate = { password ->
                // Password must contain at least one uppercase letter, one lowercase letter,
                // one digit, and one special character
                val passwordStr = password.toString()
                passwordStr.any { it.isUpperCase() } &&
                passwordStr.any { it.isLowerCase() } &&
                passwordStr.any { it.isDigit() } &&
                passwordStr.any { !it.isLetterOrDigit() }
            },
            message = "Password must include uppercase, lowercase, digit, and special character"
        )
        .end()
        
    // Address validation
    validate("customer.address.street")
        .required()
        .minLength(5)
        .end()
        
    validate("customer.address.city")
        .required()
        .end()
        
    validate("customer.address.state")
        .required()
        .pattern(Regex("^[A-Z]{2}$"))
        .end()
        
    validate("customer.address.zipCode")
        .required()
        .pattern(Regex("^\\d{5}(-\\d{4})?$"))
        .end()
        
    // Payment information
    `when`(condition = { data ->
        (data as? Map<*, *>)?.get("paymentMethod")?.toString() == "CREDIT_CARD"
    }) {
        validate("paymentInfo.cardNumber")
            .required()
            .pattern(Regex("^\\d{16}$"))
            .end()
            
        validate("paymentInfo.expiryDate")
            .required()
            .pattern(Regex("^(0[1-9]|1[0-2])/\\d{2}$"))
            .end()
            
        validate("paymentInfo.cvv")
            .required()
            .pattern(Regex("^\\d{3,4}$"))
            .end()
    }
    
    // Marketing preferences
    validate("preferences.receiveNewsletter")
        .custom(
            predicate = { it is Boolean },
            message = "Receive newsletter preference must be a boolean"
        )
        .asWarning()
        .end()
}

// Sample usage
val customerData = mapOf(
    "customer" to mapOf(
        "firstName" to "John",
        "lastName" to "Doe",
        "email" to "[email protected]",
        "password" to "P@ssw0rd",
        "address" to mapOf(
            "street" to "123 Main St",
            "city" to "Anytown",
            "state" to "NY",
            "zipCode" to "12345"
        )
    ),
    "paymentMethod" to "CREDIT_CARD",
    "paymentInfo" to mapOf(
        "cardNumber" to "4111111111111111",
        "expiryDate" to "12/25",
        "cvv" to "123"
    ),
    "preferences" to mapOf(
        "receiveNewsletter" to true
    )
)

val result = registrationValidator.validate(customerData)
if (result.isValid) {
    // Process the registration
    println("Registration is valid")
} else {
    // Handle validation errors
    println("Registration has errors:")
    result.errors.forEach { error ->
        println("- ${error.path}: ${error.message}")
    }
}

Order Processing with Validation and Mapping

// Create a complete order processing pipeline with validation
val orderProcessing = Platymap.flow("order")
    .withFormat(Format.JSON)
    .to("processedOrder")
    
    // Pre-validate the order
    .withPreValidation {
        // Basic order validation
        validate("order.id")
            .required()
            .pattern(Regex("^ORD-\\d+$"))
            .end()
            
        validate("order.customer.id")
            .required()
            .end()
            
        validate("order.date")
            .required()
            .date("yyyy-MM-dd'T'HH:mm:ss'Z'")
            .end()
            
        // Items validation
        validate("order.items")
            .required()
            .size(min = 1)
            .end()
            
        forEach("order.items") {
            validate("productId")
                .required()
                .end()
                
            validate("quantity")
                .required()
                .min(1)
                .end()
                
            validate("price")
                .required()
                .min(0.01)
                .end()
        }
        
        // Validate order total
        validateGroup(
            "order.subtotal",
            "order.tax",
            "order.total",
            message = "Order total must equal subtotal plus tax"
        ) { values ->
            val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
            val tax = (values[1] as? Number)?.toDouble() ?: 0.0
            val total = (values[2] as? Number)?.toDouble() ?: 0.0
            
            Math.abs((subtotal + tax) - total) < 0.01
        }
    }
    
    // Mapping logic
    .map("order.id").to("orderId")
    .map("order.date").to("orderDate")
    .map("order.customer.id").to("customerId")
    .map("order.customer.name").to("customerName")
    
    // Map items with validation during mapping
    .forEach("order.items")
        .`as`("item")
        .create("items")
            .map("$item.productId").to("id")
            .map("$item.name").to("description")
            .map("$item.price").to("unitPrice")
            .map("$item.quantity").to("quantity")
            .map("$item.price").transform { price ->
                val quantity = context.getValueByPath("$item.quantity") as? Number ?: 1
                val priceValue = (price as? Number)?.toDouble() ?: 0.0
                priceValue * quantity.toDouble()
            }.to("totalPrice")
        .end()
    .end()
    
    // Additional validation during mapping
    .validateDuring {
        forEach("order.items") {
            ```kotlin
            validateGroup(
                "$item.price", 
                "$item.quantity", 
                "$item.totalPrice",
                message = "Item totalPrice must equal price * quantity"
            ) { values ->
                val price = (values[0] as? Number)?.toDouble() ?: 0.0
                val quantity = (values[1] as? Number)?.toInt() ?: 0
                val totalPrice = (values[2] as? Number)?.toDouble() ?: 0.0
                
                Math.abs((price * quantity) - totalPrice) < 0.01
            }
        }
    }
    
    // Map totals
    .map("order.subtotal").to("totals.subtotal")
    .map("order.tax").to("totals.tax")
    .map("order.total").to("totals.total")
    
    // Add shipping information with conditional logic
    .branch()
        .when { data ->
            val shippingMethod = (data as? Map<*, *>)?.get("order")?.let {
                (it as? Map<*, *>)?.get("shipping")?.let { shipping ->
                    (shipping as? Map<*, *>)?.get("method")?.toString()
                }
            }
            shippingMethod == "express" || shippingMethod == "overnight"
        }
        .then()
            .map("order.shipping.trackingNumber").to("shipping.trackingCode")
            .map("shippingPriority").transform { "HIGH" }.to("shipping.priority")
        .endBranch()
        .otherwise()
            .map("order.shipping.trackingNumber").to("shipping.trackingCode")
            .map("shippingPriority").transform { "NORMAL" }.to("shipping.priority")
        .endBranch()
    .end()
    
    // Post-validate the processed order
    .withPostValidation {
        // Ensure required fields exist in output
        validate("orderId").required().end()
        validate("orderDate").required().end()
        validate("customerId").required().end()
        
        // Validate items were mapped correctly
        validate("items").required().size(min = 1).end()
        
        forEach("items") {
            validate("id").required().end()
            validate("unitPrice").required().min(0).end()
            validate("quantity").required().min(1).end()
            validate("totalPrice").required().min(0).end()
            
            // Verify calculations are correct
            validateGroup(
                "unitPrice", 
                "quantity", 
                "totalPrice",
                message = "Item total price calculation is incorrect"
            ) { values ->
                val unitPrice = (values[0] as? Number)?.toDouble() ?: 0.0
                val quantity = (values[1] as? Number)?.toInt() ?: 0
                val totalPrice = (values[2] as? Number)?.toDouble() ?: 0.0
                
                Math.abs((unitPrice * quantity) - totalPrice) < 0.01
            }
        }
        
        // Validate totals
        validate("totals.subtotal").required().min(0).end()
        validate("totals.tax").required().min(0).end()
        validate("totals.total").required().min(0).end()
        
        // Verify total calculation
        validateGroup(
            "totals.subtotal", 
            "totals.tax", 
            "totals.total",
            message = "Order total calculation is incorrect"
        ) { values ->
            val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
            val tax = (values[1] as? Number)?.toDouble() ?: 0.0
            val total = (values[2] as? Number)?.toDouble() ?: 0.0
            
            Math.abs((subtotal + tax) - total) < 0.01
        }
    }
    
    // Build with validation
    .buildWithValidation(
        ValidationConfig(
            failFast = false,
            throwOnError = true,
            includeWarnings = true
        )
    )

// Sample order data
val orderJson = """
{
    "order": {
        "id": "ORD-12345",
        "date": "2023-09-15T10:30:00Z",
        "customer": {
            "id": "CUST-789",
            "name": "Jane Smith",
            "email": "[email protected]"
        },
        "items": [
            {
                "productId": "PROD-001",
                "name": "Smartphone",
                "price": 699.99,
                "quantity": 1,
                "totalPrice": 699.99
            },
            {
                "productId": "PROD-002",
                "name": "Phone Case",
                "price": 24.99,
                "quantity": 2,
                "totalPrice": 49.98
            }
        ],
        "shipping": {
            "method": "express",
            "trackingNumber": "TRK987654321",
            "estimatedDelivery": "2023-09-18"
        },
        "subtotal": 749.97,
        "tax": 60.00,
        "total": 809.97
    }
}
"""

// Execute the validated mapping
try {
    val result = orderProcessing.executeToJson(orderJson)
    println("Order processing successful!")
    println(result)
} catch (e: ValidationException) {
    println("Order validation failed:")
    e.errors.forEach { error ->
        println("- ${error.path}: ${error.message} (${error.severity})")
    }
}

Common Validation Scenarios

Here are examples of common validation scenarios and how to handle them:

1. Form Data Validation

val formValidator = ValidationDsl.preValidate {
    // Text fields
    validate("formData.name")
        .required()
        .minLength(2)
        .maxLength(100)
        .end()
        
    validate("formData.email")
        .required()
        .email()
        .end()
        
    // Phone with optional format
    validate("formData.phone")
        .pattern(Regex("^\\+?[0-9\\s-()]{10,20}$"))
        .asWarning()  // Only warn for invalid format
        .end()
        
    // Selection field
    validate("formData.category")
        .required()
        .allowedValues("personal", "business", "education", "other")
        .end()
        
    // Number field
    validate("formData.age")
        .min(18)
        .max(120)
        .end()
        
    // Checkbox confirmation
    validate("formData.termsAccepted")
        .custom(
            predicate = { it == true },
            message = "You must accept the terms and conditions"
        )
        .end()
        
    // Date field
    validate("formData.eventDate")
        .date("yyyy-MM-dd")
        .custom(
            predicate = { date ->
                // Must be a future date
                if (date == null) return@custom true
                val parsedDate = java.time.LocalDate.parse(
                    date.toString(),
                    java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")
                )
                parsedDate.isAfter(java.time.LocalDate.now())
            },
            message = "Event date must be in the future"
        )
        .end()
}

2. API Response Validation

val apiResponseValidator = ValidationDsl.postValidate {
    // Check API status
    validate("status")
        .required()
        .allowedValues("success", "error", "pending")
        .end()
        
    // Conditional validation based on status
    `when`(condition = { data ->
        (data as? Map<*, *>)?.get("status") == "success"
    }) {
        validate("data")
            .required()
            .end()
            
        // Check pagination if present
        `when`(condition = { data ->
            (data as? Map<*, *>)?.get("data")?.let {
                (it as? Map<*, *>)?.containsKey("pagination")
            } == true
        }) {
            validate("data.pagination.total")
                .required()
                .min(0)
                .end()
                
            validate("data.pagination.page")
                .required()
                .min(1)
                .end()
                
            validate("data.pagination.perPage")
                .required()
                .min(1)
                .end()
                
            // Verify pagination math
            validateGroup(
                "data.pagination.total",
                "data.pagination.perPage",
                "data.pagination.totalPages",
                message = "Pagination calculation is incorrect"
            ) { values ->
                val total = (values[0] as? Number)?.toInt() ?: 0
                val perPage = (values[1] as? Number)?.toInt() ?: 1
                val totalPages = (values[2] as? Number)?.toInt() ?: 0
                
                val expectedTotalPages = Math.ceil(total.toDouble() / perPage).toInt()
                expectedTotalPages == totalPages
            }
        }
    }
    
    // Error response validation
    `when`(condition = { data ->
        (data as? Map<*, *>)?.get("status") == "error"
    }) {
        validate("error")
            .required()
            .end()
            
        validate("error.code")
            .required()
            .end()
            
        validate("error.message")
            .required()
            .end()
    }
}

3. Configuration Validation

val configValidator = ValidationDsl.preValidate {
    // Database configuration
    validate("config.database.url")
        .required()
        .pattern(Regex("^jdbc:[a-zA-Z0-9:]+://[^\\s/$.?#]+\\.[^\\s]+$"))
        .end()
        
    validate("config.database.username")
        .required()
        .end()
        
    validate("config.database.password")
        .required()
        .minLength(8)
        .end()
        
    // Server configuration
    validate("config.server.port")
        .required()
        .min(1024)
        .max(65535)
        .end()
        
    validate("config.server.maxThreads")
        .required()
        .min(1)
        .max(1000)
        .end()
        
    // Feature flags
    validateAll("config.features.*")
        .custom(
            predicate = { it is Boolean },
            message = "Feature flags must be boolean values"
        )
        .end()
        
    // Logging levels
    validate("config.logging.level")
        .required()
        .allowedValues("DEBUG", "INFO", "WARN", "ERROR")
        .end()
        
    // File paths
    validateAll("config.paths.*")
        .custom(
            predicate = { path ->
                path?.toString()?.let { 
                    java.io.File(it).exists() 
                } ?: false
            },
            message = "Path must exist on the filesystem"
        )
        .asWarning()  // Only warn for non-existent paths
        .end()
}

Conclusion

The PlatyMap Validation DSL provides a powerful, flexible way to ensure your data meets all requirements before, during, and after mapping operations. By combining validation with mapping, you can create robust data transformation pipelines that catch errors early and produce reliable results. For more advanced usage, refer to the API documentation or explore the source code to understand the full capabilities of the validation framework.