ADR-031: Request Validation
Status
Implemented
Date
2025-01-16 (Retrospective)
Decision Makers
- Backend Team - Validation strategy
- Security Team - Input sanitization
Layer
API
Related ADRs
- ADR-008: FastAPI with Pydantic
Supersedes
None
Depends On
- ADR-008: FastAPI with Pydantic
Context
API security requires strict input validation:
- Type Safety: Ensure correct data types
- Constraint Enforcement: Min/max, regex patterns
- Injection Prevention: SQL, XSS, command injection
- Clear Errors: Helpful validation messages
- 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
- Pydantic V2: Latest version with Rust core
- Schema Models: Separate Create/Update/Response schemas
- Field Validators: Custom validation functions
- OpenAPI Integration: Auto-generated from models
- 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:
- Mass Assignment Risk: Without
extra='forbid', attackers can inject unexpected fields - Secret Handling: Plain
strfor API keys/passwords risks logging exposure - Validation ≠ Injection Prevention: Pydantic validates types, doesn't sanitize for SQL/XSS
- Missing URL Validation: Database URLs should use
PostgresDsntype
Required Modifications:
- Add
extra='forbid': Reject unknown fields in all schemasmodel_config = ConfigDict(extra='forbid', str_strip_whitespace=True) - Use SecretStr: For sensitive fields that must be masked in logs
api_key: SecretStr | None = None - URL Types: Use
PostgresDsn,RedisDsnfor connection strings - Sanitization Layer: Add explicit sanitization for fields used in queries/templates
- Length Limits: Ensure all string fields have
max_lengthto prevent DoS
Modifications Applied
- Documented
extra='forbid'requirement - Added SecretStr for sensitive fields
- Documented URL type validation
- 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