Skip to content

Cross-Field Validation — Password Match, Date Range, and Dependencies

DodaTech Updated 2026-06-28 9 min read

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

What is cross-field validation?

Validation that checks relationships between multiple fields, ensuring consistency across the entire data object.

How do I validate password and confirm password?

Check both fields exist, compare for equality, and return a clear mismatch error. Always check the confirm field.

Should I validate date ranges before or after individual fields?

Validate individual fields first (format, type), then cross-field rules (range, ordering).

How do conditional required fields work?

Field B is required only when Field A has a specific value. Check the condition before applying the requirement.

Can cross-field validation be automated?

Yes, using a rule engine that registers multi-field validators and runs them on the complete data object.

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