Skip to main content

ADR-031: Request Validation

Status

Implemented

Date

2025-01-16 (Retrospective)

Decision Makers

  • Backend Team - Validation strategy
  • Security Team - Input sanitization

Layer

API

  • ADR-008: FastAPI with Pydantic

Supersedes

None

Depends On

  • ADR-008: FastAPI with Pydantic

Context

API security requires strict input validation:

  1. Type Safety: Ensure correct data types
  2. Constraint Enforcement: Min/max, regex patterns
  3. Injection Prevention: SQL, XSS, command injection
  4. Clear Errors: Helpful validation messages
  5. Performance: Fail fast on invalid input

Requirements:

  • Validate at API boundary
  • Consistent error format
  • Field-level error messages
  • Custom validators for business rules
  • OpenAPI schema generation

Decision

We use Pydantic models for comprehensive request validation:

Key Design Decisions

  1. Pydantic V2: Latest version with Rust core
  2. Schema Models: Separate Create/Update/Response schemas
  3. Field Validators: Custom validation functions
  4. OpenAPI Integration: Auto-generated from models
  5. Error Format: Standard FastAPI 422 response

Schema Pattern

from pydantic import BaseModel, Field, field_validator

class RequirementCreate(BaseModel):
"""Schema for creating a requirement."""

title: str = Field(
...,
min_length=1,
max_length=500,
description="Requirement title"
)
description: str | None = Field(
None,
max_length=10000,
description="Detailed description"
)
type: RequirementType = Field(
default=RequirementType.FUNCTIONAL,
description="Type of requirement"
)
priority: Priority = Field(
default=Priority.MEDIUM,
description="Priority level"
)

@field_validator('title')
@classmethod
def title_not_empty(cls, v):
if not v.strip():
raise ValueError('Title cannot be empty or whitespace')
return v.strip()

model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)

Validation Error Response

HTTP 422 Unprocessable Entity

{
"detail": [
{
"type": "string_too_short",
"loc": ["body", "title"],
"msg": "String should have at least 1 character",
"input": "",
"ctx": {"min_length": 1}
}
]
}

Custom Validators

class RequirementUpdate(BaseModel):
@field_validator('target_date')
@classmethod
def target_date_future(cls, v):
if v and v < date.today():
raise ValueError('Target date must be in the future')
return v

@model_validator(mode='after')
def check_dependencies(self):
if self.status == 'completed' and not self.completion_date:
raise ValueError('Completion date required for completed status')
return self

Consequences

Positive

  • Type Safety: Invalid types rejected at boundary
  • Clear Errors: Field-specific error messages
  • Documentation: OpenAPI schemas auto-generated
  • Consistency: Same validation everywhere
  • Performance: Fast Rust-based validation

Negative

  • Schema Proliferation: Many schema classes
  • Sync Required: Schemas must match models
  • Complex Validators: Custom logic can be verbose
  • Learning Curve: Pydantic patterns to learn

Neutral

  • Error Format: Standard but verbose
  • Migration: V2 differs from V1 syntax

Implementation Status

  • Core implementation complete
  • Tests written and passing
  • Documentation updated
  • Migration/upgrade path defined
  • Monitoring/observability in place

Implementation Details

  • Schemas: backend/schemas/
  • Validators: Within schema classes
  • OpenAPI: Auto-generated at /docs
  • Tests: backend/tests/unit/test_schemas.py

LLM Council Review

Review Date: 2025-01-16 Confidence Level: High (90%) Verdict: APPROVED WITH MODIFICATIONS

Quality Metrics

  • Consensus Strength Score (CSS): 0.85
  • Deliberation Depth Index (DDI): 0.88

Council Feedback Summary

Pydantic V2 is the correct choice with its Rust core providing fast validation. However, critical security gaps exist around mass assignment and secret handling.

Key Concerns Identified:

  1. Mass Assignment Risk: Without extra='forbid', attackers can inject unexpected fields
  2. Secret Handling: Plain str for API keys/passwords risks logging exposure
  3. Validation ≠ Injection Prevention: Pydantic validates types, doesn't sanitize for SQL/XSS
  4. Missing URL Validation: Database URLs should use PostgresDsn type

Required Modifications:

  1. Add extra='forbid': Reject unknown fields in all schemas
    model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
  2. Use SecretStr: For sensitive fields that must be masked in logs
    api_key: SecretStr | None = None
  3. URL Types: Use PostgresDsn, RedisDsn for connection strings
  4. Sanitization Layer: Add explicit sanitization for fields used in queries/templates
  5. Length Limits: Ensure all string fields have max_length to prevent DoS

Modifications Applied

  1. Documented extra='forbid' requirement
  2. Added SecretStr for sensitive fields
  3. Documented URL type validation
  4. Added sanitization guidance

Council Ranking

  • gpt-5.2: Best Response (security gaps)
  • gemini-3-pro: Strong (mass assignment)
  • grok-4.1: Good (practical fixes)

References


ADR-031 | API Layer | Implemented