Skip to content

Input Validation

Pydantic schema validation on all API inputs with custom validators and react-admin-compatible error responses.

Validation Architecture

All API input goes through three validation layers:

  1. Pydantic schemas — Type checking and custom validators on request bodies
  2. Django ORM — Database constraints (unique, FK, check constraints)
  3. Service layer — Business rule validation (state transitions, balances)

Pydantic Schema Validation

Every API endpoint defines input schemas with typed fields and validators:

class BorrowerIn(Schema):
    first_name: str = Field(description="Borrower's first name")
    email: Annotated[
        str,
        Field(description="Email address"),
        AfterValidator(validate_email_format),
    ]
    ssn_last_four: Annotated[
        str,
        Field(description="Last four digits of SSN"),
        AfterValidator(validate_ssn_last_four),
    ]

django-ninja-extra handles Pydantic validation automatically — invalid requests receive a 422 response before reaching the service layer.

Common Validators

common/validators.py provides reusable pure-function validators:

Validator Rule Example
validate_email_format Django email validator user@example.com
validate_phone_e164 Regex ^\+[1-9]\d{7,14}$ +12025551234
validate_positive_decimal Strictly > 0 Decimal("100.00")
validate_non_negative_decimal >= 0 Decimal("0.00")
validate_rate_decimal 0 <= value <= 1 Decimal("0.065")
validate_ssn_last_four Regex ^\d{4}$ "1234"
validate_timezone Valid IANA timezone "America/New_York"
validate_content_type_string app_label.model_name format "borrowers.borrower"
validate_meta_data_dict Flat dict, max 50 keys, scalar values {"key": "value"}

Type-Annotated Aliases

Pre-built Annotated types for common patterns:

from common.validators import PositiveDecimal, NonNegativeDecimal, RateDecimal, TimezoneStr

class FeeIn(Schema):
    amount: PositiveDecimal = Field(description="Fee amount")
    rate: RateDecimal = Field(description="Fee rate (0.0 to 1.0)")

DualValidationError

Validators raise DualValidationError(ValueError, ValidationError) which is catchable by both Pydantic (as ValueError) and Django (as ValidationError).

Validation Error Format

Pydantic validation errors are converted to a react-admin-compatible format:

{
  "message": "Validation failed",
  "errors": {
    "email": "Enter a valid email address.",
    "ssn_last_four": "Must be exactly 4 digits.",
    "parties[2].role": "Value is not a valid enumeration member."
  }
}

HTTP 422 Unprocessable Entity

Error Message Mapping

CUSTOM_MESSAGES in config/api.py maps Pydantic error type codes to user-friendly messages:

Pydantic Code User Message
string_type "This field is required."
missing "This field is required."
value_error Validator's message
enum "Value is not a valid enumeration member."

Field Path Extraction

Pydantic's loc tuple is converted to dot-notation paths:

  • ("body", "payload", "email")"email" (strips body prefix + parameter name)
  • ("body", "payload", "parties", 2, "role")"parties[2].role" (bracket notation for indices)

Business Logic Errors

Service-layer exceptions are mapped to appropriate HTTP status codes:

Exception HTTP Status When
ObjectDoesNotExist 404 Resource not found
ValidationError 422 Field-level validation failure
InvalidStateTransitionError 409 Invalid status change
ComplianceBlockError 409 Compliance check failed
ConcurrencyConflictError 409 Optimistic locking conflict
IntegrityError 409 Database constraint violation (duplicate)
ResourceLockedError 423 Resource locked (with Retry-After header)
ProcessorError 502 External payment processor error
BusinessLogicError 400 General business rule violation

SQL Injection Prevention

All database queries use Django's ORM with parameterized queries. Raw SQL is never constructed from user input:

  • ORM methods (filter(), get(), create()) automatically parameterize
  • The few RunSQL migrations use static SQL strings
  • No raw() or extra() calls with user-provided values

See Also