Cross-Field Validation — Password Match, Date Range, and Dependencies
In this tutorial, you'll learn about Cross-Field Validation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Cross-field validation verifies relationships between multiple fields, ensuring data consistency where the value of one field depends on another.
What You'll Learn
By the end of this lesson, you will implement password match checks, date range validation, conditional field requirements, and multi-field business rules.
Why It Matters
Individual field validation is not enough. A confirmed password that does not match, an end date before the start date, or an invalid address combination can corrupt business logic and user data.
Real-World Use
Durga Antivirus Pro validates that a license expiration date must be after the issue date, and that the maximum scan count cannot exceed the license tier limit — both cross-field rules.
Cross-Field Validation Flow
flowchart TD
Data[Full Data Object] --> Field1[Validate Field A]
Data --> Field2[Validate Field B]
Data --> Relationship{Cross-Field Rules}
Relationship --> Match[Password Match]
Relationship --> Date[Date Range Check]
Relationship --> Conditional[Conditional Required]
Relationship --> Business[Business Rules]
Match --> Result[Error Collection]
Date --> Result
Conditional --> Result
Business --> Result
Result --> Pass{Any Errors?}
Pass -->|Yes| Reject[Reject Request]
Pass -->|No| Accept[Process Request]
Password and Confirmation Match
# password_match.py
from typing import Any, Dict, List, Optional
class PasswordMatchValidator:
@staticmethod
def validate(password: Any, confirm_password: Any) -> Optional[str]:
if not isinstance(password, str) or not isinstance(confirm_password, str):
return "Both password fields must be strings"
if password != confirm_password:
return "Password and confirmation do not match"
if len(password) < 8:
return "Password must be at least 8 characters"
return None
@staticmethod
def validate_with_data(data: Dict) -> List[Dict]:
errors = []
password = data.get("password")
confirm = data.get("confirm_password")
if password is None or confirm is None:
if password is None:
errors.append({"field": "password", "code": "required"})
if confirm is None:
errors.append({"field": "confirm_password", "code": "required"})
return errors
if password != confirm:
errors.append({"field": "confirm_password", "code": "password_mismatch", "message": "Passwords do not match"})
if len(password) < 8:
errors.append({"field": "password", "code": "too_short", "message": "Minimum 8 characters"})
return errors
pm = PasswordMatchValidator()
test_cases = [
{"password": "Secret123!", "confirm_password": "Secret123!"},
{"password": "Secret123!", "confirm_password": "Different!"},
{"password": "Abc12!", "confirm_password": "Abc12!"},
{"password": "Secret123!"},
]
for case in test_cases:
errors = pm.validate_with_data(case)
if not errors:
print(f" {case} -> VALID")
else:
print(f" {case} -> {errors}")
Expected output:
{'password': 'Secret123!', 'confirm_password': 'Secret123!'} -> VALID
{'password': 'Secret123!', 'confirm_password': 'Different!'} -> [{'field': 'confirm_password', 'code': 'password_mismatch', 'message': 'Passwords do not match'}]
{'password': 'Abc12!', 'confirm_password': 'Abc12!'} -> [{'field': 'password', 'code': 'too_short', 'message': 'Minimum 8 characters'}]
{'password': 'Secret123!'} -> [{'field': 'confirm_password', 'code': 'required'}]
Date Range Validation
# date_range.py
from datetime import datetime, date
from typing import Any, Dict, List, Optional
class DateRangeValidator:
DATE_FORMATS = ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%m/%d/%Y"]
@staticmethod
def parse_date(value: Any) -> Optional[date]:
if isinstance(value, (datetime, date)):
return value if isinstance(value, date) else value.date()
if not isinstance(value, str):
return None
for fmt in DateRangeValidator.DATE_FORMATS:
try:
return datetime.strptime(value.strip(), fmt).date()
except ValueError:
continue
return None
@staticmethod
def validate_range(start: Any, end: Any, start_field: str = "start_date",
end_field: str = "end_date") -> List[Dict]:
errors = []
start_date = DateRangeValidator.parse_date(start)
end_date = DateRangeValidator.parse_date(end)
if start_date is None:
errors.append({"field": start_field, "code": "invalid_date", "message": f"Invalid {start_field} format"})
if end_date is None:
errors.append({"field": end_field, "code": "invalid_date", "message": f"Invalid {end_field} format"})
if start_date is not None and end_date is not None and start_date > end_date:
errors.append({
"field": end_field,
"code": "end_before_start",
"message": f"{end_field} must be after {start_field}"
})
return errors
@staticmethod
def validate_future(value: Any, field: str = "date") -> Optional[str]:
parsed = DateRangeValidator.parse_date(value)
if parsed is None:
return f"Invalid {field} format"
if parsed < date.today():
return f"{field} must be in the future"
return None
drv = DateRangeValidator()
tests = [
("2024-01-01", "2024-12-31"),
("2024-12-31", "2024-01-01"),
("invalid", "2024-12-31"),
("2024-01-01", None),
]
for start, end in tests:
errors = drv.validate_range(start, end)
status = "VALID" if not errors else errors
print(f" start={start} end={end} -> {status}")
print(f" Future date '2099-01-01': {drv.validate_future('2099-01-01')}")
print(f" Past date '2020-01-01': {drv.validate_future('2020-01-01')}")
Expected output:
start=2024-01-01 end=2024-12-31 -> VALID
start=2024-12-31 end=2024-01-01 -> [{'field': 'end_date', 'code': 'end_before_start', 'message': 'end_date must be after start_date'}]
start=invalid end=2024-12-31 -> [{'field': 'start_date', 'code': 'invalid_date', 'message': 'Invalid start_date format'}]
start=2024-01-01 end=None -> [{'field': 'end_date', 'code': 'invalid_date', 'message': 'Invalid end_date format'}]
Future date '2099-01-01': None
Past date '2020-01-01': date must be in the future
Conditional Dependent Field Validation
# conditional_dependent.py
from typing import Any, Dict, List, Optional
class ConditionalDependentValidator:
@staticmethod
def validate_shipping(data: Dict) -> List[Dict]:
errors = []
method = data.get("shipping_method")
if method == "physical":
if not data.get("address"):
errors.append({"field": "address", "code": "required_when_physical"})
if not data.get("city"):
errors.append({"field": "city", "code": "required_when_physical"})
if not data.get("zip"):
errors.append({"field": "zip", "code": "required_when_physical"})
if method == "digital":
if not data.get("email"):
errors.append({"field": "email", "code": "required_when_digital"})
if method == "pickup":
if not data.get("store_id"):
errors.append({"field": "store_id", "code": "required_when_pickup"})
if not method:
errors.append({"field": "shipping_method", "code": "required"})
return errors
@staticmethod
def validate_payment(data: Dict) -> List[Dict]:
errors = []
method = data.get("payment_method")
if method == "card":
if not data.get("card_number"):
errors.append({"field": "card_number", "code": "required_when_card"})
if not data.get("cvv"):
errors.append({"field": "cvv", "code": "required_when_card"})
if len(data.get("card_number", "")) != 16:
errors.append({"field": "card_number", "code": "invalid_length"})
if method == "bank_transfer":
if not data.get("account_number"):
errors.append({"field": "account_number", "code": "required_when_bank"})
if not data.get("routing_number"):
errors.append({"field": "routing_number", "code": "required_when_bank"})
if method == "crypto" and not data.get("wallet_address"):
errors.append({"field": "wallet_address", "code": "required_when_crypto"})
return errors
cdv = ConditionalDependentValidator()
shipping_tests = [
{"shipping_method": "physical", "address": "123 Main St", "city": "NYC", "zip": "10001"},
{"shipping_method": "physical", "address": "123 Main St"},
{"shipping_method": "digital", "email": "user@example.com"},
{"shipping_method": "digital"},
{"shipping_method": "pickup"},
{},
]
for test in shipping_tests:
errors = cdv.validate_shipping(test)
status = "VALID" if not errors else errors
print(f" {test} -> {status}")
payment_test = {"payment_method": "card", "card_number": "1234567890123456", "cvv": "123"}
print(f"\n Payment card:{cdv.validate_payment(payment_test)}")
payment_test2 = {"payment_method": "card", "card_number": "1234"}
print(f" Payment card bad: {cdv.validate_payment(payment_test2)}")
Expected output:
{'shipping_method': 'physical', 'address': '123 Main St', 'city': 'NYC', 'zip': '10001'} -> VALID
{'shipping_method': 'physical', 'address': '123 Main St'} -> [{'field': 'city', 'code': 'required_when_physical'}, {'field': 'zip', 'code': 'required_when_physical'}]
{'shipping_method': 'digital', 'email': 'user@example.com'} -> VALID
{'shipping_method': 'digital'} -> [{'field': 'email', 'code': 'required_when_digital'}]
{'shipping_method': 'pickup'} -> [{'field': 'store_id', 'code': 'required_when_pickup'}]
{} -> [{'field': 'shipping_method', 'code': 'required'}]
Payment card:[]
Payment card bad: [{'field': 'card_number', 'code': 'invalid_length'}]
Cross-Field Rule Engine
# cross_field_engine.py
from typing import Any, Callable, Dict, List, Optional
class CrossFieldRule:
def __init__(self, name: str, fields: List[str],
validate_fn: Callable[[Dict], Optional[str]]):
self.name = name
self.fields = fields
self.validate_fn = validate_fn
class CrossFieldEngine:
def __init__(self):
self.rules: List[CrossFieldRule] = []
def add_rule(self, rule: CrossFieldRule):
self.rules.append(rule)
def validate(self, data: Dict) -> Dict[str, Any]:
results = {"valid": True, "errors": []}
for rule in self.rules:
error = rule.validate_fn(data)
if error:
results["valid"] = False
results["errors"].append({
"rule": rule.name,
"fields": rule.fields,
"message": error
})
return results
engine = CrossFieldEngine()
engine.add_rule(CrossFieldRule(
"password_match", ["password", "confirm_password"],
lambda d: None if d.get("password") == d.get("confirm_password") else "Passwords do not match"
))
engine.add_rule(CrossFieldRule(
"end_after_start", ["start_date", "end_date"],
lambda d: None if d.get("end_date", "") >= d.get("start_date", "") else "End date must be after start"
))
engine.add_rule(CrossFieldRule(
"adult_age", ["age"],
lambda d: None if d.get("age", 0) >= 18 else "Must be 18 or older"
))
engine.add_rule(CrossFieldRule(
"us_address_zip", ["country", "zip"],
lambda d: None if d.get("country") != "US" or len(d.get("zip", "")) >= 5 else "Invalid US ZIP"
))
tests = [
{"password": "a", "confirm_password": "a", "start_date": "2024-01-01", "end_date": "2024-12-31", "age": 25, "country": "US", "zip": "90210"},
{"password": "a", "confirm_password": "b", "start_date": "2024-12-31", "end_date": "2024-01-01", "age": 16, "country": "US", "zip": "90"},
]
for test in tests:
result = engine.validate(test)
print(f" {result['valid']} -> {result['errors']}")
Expected output:
True -> []
False -> [{'rule': 'password_match', 'fields': ['password', 'confirm_password'], 'message': 'Passwords do not match'}, {'rule': 'end_after_start', 'fields': ['start_date', 'end_date'], 'message': 'End date must be after start'}, {'rule': 'adult_age', 'fields': ['age'], 'message': 'Must be 18 or older'}, {'rule': 'us_address_zip', 'fields': ['country', 'zip'], 'message': 'Invalid US ZIP'}]
Common Mistakes
1. Only Checking Individual Fields
Cross-field logic is often missed when validators are designed per-field. Always validate the complete data object.
2. Password Mismatch Without Confirming Both Present
If confirm_password is missing entirely, the mismatch check passes. Always check both fields exist before comparing.
3. Date Comparison Without Parsing
Comparing date strings lexicographically works for ISO 8601 but fails for other formats. Always parse before comparison.
4. Order-Dependent Validation
Failing to validate field A before using it in field B's conditional check. Validate prerequisites first.
5. Not Collecting All Errors
Returning on the first cross-field error prevents the client from seeing all issues. Collect and return all errors.
Practice Questions
1. What is cross-field validation?
Validation that checks relationships between multiple fields, such as password match or date range.
2. Why should you check both password fields exist before comparing?
If confirm_password is missing, the comparison returns False and produces a confusing error. Check presence first.
3. How do you validate date ranges safely?
Parse dates into date objects first, then compare. Do not compare date strings directly.
4. What is a conditional dependent field?
A field that is required only when another field has a specific value, like address being required only when shipping is physical.
Challenge
Build a cross-field validation system for a booking system: check-in before check-out, adult count consistent with room capacity, ages of guests require adult supervision if under 18, and payment method consistent with booking type.
FAQ
Mini Project: Registration Validator with Cross-Field Rules
# registration_validator.py
from typing import Any, Dict, List, Optional
class RegistrationValidator:
def validate(self, data: Dict) -> List[Dict]:
errors = []
# Individual field checks
if not data.get("username"):
errors.append({"field": "username", "code": "required"})
if not data.get("email"):
errors.append({"field": "email", "code": "required"})
# Cross-field: password match
pw = data.get("password")
cpw = data.get("confirm_password")
if pw or cpw:
if pw != cpw:
errors.append({"field": "confirm_password", "code": "mismatch"})
if pw and len(pw) < 8:
errors.append({"field": "password", "code": "too_short"})
else:
errors.append({"field": "password", "code": "required"})
# Cross-field: age and country
age = data.get("age")
country = data.get("country")
if country == "US" and age is not None and age < 13:
errors.append({"field": "age", "code": "coppa_violation", "message": "US users must be 13+"})
return errors
rv = RegistrationValidator()
print(rv.validate({"username": "alice", "email": "a@x.com", "password": "Secret1!", "confirm_password": "Secret1!", "country": "US", "age": 10}))
print(rv.validate({"username": "bob", "email": "b@x.com", "password": "Secret1!", "confirm_password": "Secret1!", "country": "US", "age": 25}))
print(rv.validate({"username": "charlie", "password": "abc", "confirm_password": "xyz"}))
Expected output:
[{'field': 'age', 'code': 'coppa_violation', 'message': 'US users must be 13+'}]
[]
[{'field': 'email', 'code': 'required'}, {'field': 'confirm_password', 'code': 'mismatch'}, {'field': 'password', 'code': 'too_short'}]
What's Next
You understand cross-field validation. Next, learn custom validation rules, then validation libraries comparison.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro