Handoff: API Refactor Phase 2 — Pydantic Response Schemas¶
Branch: refactor/api-arjan-phase2
Status: Complete — 188/188 tests passing, flake8 clean
What Was Done¶
New file: api/schemas/responses.py¶
Created Pydantic v2 response schemas with from_attributes=True and populate_by_name=True. All schemas inherit from BaseResponse.
Schemas created:
HealthResponse—{status, message}ContactResponse—{success, message}FeatureFlagsResponse—{cvGeneration, portfolioContentApi}ProjectPhase—{name, description}ProjectCard— all project card fields withlongDescription(validation_alias:long_description)ProjectDetail— full project detail with timeline decoding and prevId/nextId supportWorkEntry— work history entry withadditionalProjects(validation_alias:additional_projects)Skill—{name, projects}SkillCategory— withiconName/iconColor(validation_aliases:icon_name,icon_color)Award— withiconName/bgColor(validation_aliases:icon_name,bg_color)Certification— withiconType/linkText(validation_aliases:icon_type,link_text)
Updated: api/schemas/__init__.py¶
Added explicit re-exports of all response schemas.
Updated: api/main.py¶
Imports changed:
- ORM models renamed at import time to avoid name collision with response schemas:
Award as AwardORM,Certification as CertificationORM,SkillCategory as SkillCategoryORM,WorkEntry as WorkEntryORM- Added
from api.schemas.responses import ...for all response schema classes
Response model classes removed (replaced by api/schemas/responses.py):
ContactResponse,HealthResponse,ProjectCard,ProjectPhase,ProjectDetailWorkEntryModel,SkillModel,SkillCategoryModel,CertificationModel,AwardModelFeatureFlagsResponse
Converter helpers deleted:
_project_to_card— replaced byProjectCard.model_validate(p)_decode_timeline— logic moved intoProjectDetail._decode_timelinefield_validator_project_to_detail— replaced by_project_detail_from_orm(p, prev_id, next_id)_work_entry_to_model— replaced byWorkEntry.model_validate(w)_skill_cat_to_model— replaced bySkillCategory.model_validate(c)_award_to_model— replaced byAward.model_validate(a)_cert_to_model— replaced byCertification.model_validate(c)
Key Design Decisions¶
validation_alias vs alias¶
Used Field(validation_alias="snake_case") rather than Field(alias="snake_case").
Why: FastAPI serializes responses with model_dump(by_alias=True). When using
Field(alias=...), the alias becomes the JSON output key. With Field(validation_alias=...),
the alias is only used for reading (ORM attribute lookup), and the field name (camelCase)
is used for JSON output — which is what the frontend expects.
UUID coercion in BaseResponse¶
A model_validator(mode="before") in BaseResponse detects when the input is an ORM
object with a UUID id field, converts it to a plain dict (via __dict__), and stringifies
the UUID. This avoids the need for str(p.id) at every call site.
The dict-based approach is used because from_attributes mode with mode="before" validators
expects either a dict or the original object — returning a dict from a mode="before" validator
is the correct Pydantic v2 pattern.
ProjectDetail timeline handling¶
The ORM stores timeline phases in timeline_encoded: list[str] as "name|||description" strings.
The ProjectDetail._decode_timeline field_validator decodes these to list[ProjectPhase].
Since prevId/nextId are not ORM columns, the call site uses a plain dict:
This is encapsulated in the thin helper _project_detail_from_orm(p, prev_id, next_id) which
remains in main.py. It is not a "converter" in the Phase 1 sense — it only exists because
two computed fields (prevId, nextId) cannot come from the ORM object alone.
Static data unchanged¶
The large static data dictionaries (PROJECT_CARDS, WORK_HISTORY, etc.) continue to use
keyword-argument construction (e.g. SkillCategory(iconName="Cloud", ...)). This works
because populate_by_name=True allows using the field name (iconName) in addition to the
validation_alias (icon_name).
What Remains (if not done)¶
Phase 2 is complete. Potential future improvements:
- Phase 3: Move static data lists out of
main.pyinto dedicated seed modules or a separateapi/data/package. - Phase 4: Extract endpoint handlers into the router files under
api/routers/. - Phase 5: Move CV generation logic into
api/services/cv.py.