Skip to content

Conflict between Pydantic serialization aliases (camelCase/snake_case) and Cadwyn's response validation #370

@Kolovatoff

Description

@Kolovatoff

Describe the bug

When using Pydantic models with serialization_alias (e.g., converting snake_case to camelCase for API responses), Cadwyn's response validation fails with ResponseValidationError: Field required.

The error occurs because Cadwyn's internal validation process expects fields in snake_case (as defined in the Python model), but the actual response data has already been transformed to camelCase by Pydantic's alias_generator.
Error stacktrace (excerpt):

  {'type': 'missing', 'loc': ('response', 'data', 0, 'external_id'), 'msg': 'Field required', 'input': {'id': UUID('ad33be56-5bf8-4def-b355-e8a9e9500c00'), 'externalId': UUID('d2a23deb-41d0-40bc-ad36-960b842f9708'), 'title': 'First item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588627, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588632, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}
  {'type': 'missing', 'loc': ('response', 'data', 0, 'created_at'), 'msg': 'Field required', 'input': {'id': UUID('ad33be56-5bf8-4def-b355-e8a9e9500c00'), 'externalId': UUID('d2a23deb-41d0-40bc-ad36-960b842f9708'), 'title': 'First item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588627, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588632, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}
  {'type': 'missing', 'loc': ('response', 'data', 0, 'updated_at'), 'msg': 'Field required', 'input': {'id': UUID('ad33be56-5bf8-4def-b355-e8a9e9500c00'), 'externalId': UUID('d2a23deb-41d0-40bc-ad36-960b842f9708'), 'title': 'First item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588627, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588632, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}
  {'type': 'missing', 'loc': ('response', 'data', 1, 'external_id'), 'msg': 'Field required', 'input': {'id': UUID('51fd8b72-7823-47a3-b259-88cb5fba3ec5'), 'externalId': UUID('1c2983f0-0a12-4591-a4a3-7541c944bc17'), 'title': 'Second item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}
  {'type': 'missing', 'loc': ('response', 'data', 1, 'created_at'), 'msg': 'Field required', 'input': {'id': UUID('51fd8b72-7823-47a3-b259-88cb5fba3ec5'), 'externalId': UUID('1c2983f0-0a12-4591-a4a3-7541c944bc17'), 'title': 'Second item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}
  {'type': 'missing', 'loc': ('response', 'data', 1, 'updated_at'), 'msg': 'Field required', 'input': {'id': UUID('51fd8b72-7823-47a3-b259-88cb5fba3ec5'), 'externalId': UUID('1c2983f0-0a12-4591-a4a3-7541c944bc17'), 'title': 'Second item', 'createdAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'updatedAt': datetime.datetime(2026, 4, 16, 20, 51, 27, 588642, tzinfo=zoneinfo.ZoneInfo(key='UTC'))}}

Key observation: The input data contains camelCase fields (externalId, createdAt, updatedAt), but Cadwyn expects snake_case (external_id, created_at, updated_at).

To Reproduce

1. Create py file with:

from datetime import date, datetime
from uuid import UUID, uuid4
from zoneinfo import ZoneInfo

from cadwyn import (
    Cadwyn,
    HeadVersion,
    Version,
    VersionBundle,
    VersionedAPIRouter,
)
from fastapi import status
from pydantic import AliasGenerator, BaseModel, ConfigDict
from pydantic.alias_generators import to_camel, to_snake


class SnakeToCamelModel(BaseModel):
    model_config = ConfigDict(
        alias_generator=AliasGenerator(
            validation_alias=to_snake,
            serialization_alias=to_camel,
        ),
    )


class ItemRead(SnakeToCamelModel):
    id: UUID
    external_id: UUID
    title: str
    created_at: datetime
    updated_at: datetime


class IGetResponsePaginated(SnakeToCamelModel):
    data: list[ItemRead]
    meta: dict | None = None
    message: str = ""


router = VersionedAPIRouter()

fake_data = [
    {
        "id": uuid4(),
        "external_id": uuid4(),
        "title": "First item",
        "created_at": datetime.now(ZoneInfo("UTC")),
        "updated_at": datetime.now(ZoneInfo("UTC")),
    },
    {
        "id": uuid4(),
        "external_id": uuid4(),
        "title": "Second item",
        "created_at": datetime.now(ZoneInfo("UTC")),
        "updated_at": datetime.now(ZoneInfo("UTC")),
    },
]


@router.get(
    "/items", response_model=IGetResponsePaginated, status_code=status.HTTP_200_OK
)
async def read_item_list():
    return IGetResponsePaginated(
        data=[ItemRead(**item) for item in fake_data],
        meta={"page": 1, "per_page": 10, "total": len(fake_data)},
        message="Data paginated correctly",
    )


versions = VersionBundle(
    HeadVersion(),
    Version(date(2026, 1, 1)),
)

app = Cadwyn(
    versions=versions,
)

app.generate_and_include_versioned_routers(router)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="localhost", port=8000)

2. Make a request to the endpoint:

curl -X GET http://localhost:8000/items -H "X-API-VERSION: 2026-01-01"

3. See error:
Cadwyn fails to validate the response because it expects snake_case fields but receives camelCase.

Expected behavior

Cadwyn should respect Pydantic's serialization_alias configuration and validate responses after the serialization aliases have been applied, or provide a way to configure the expected field naming convention for response validation.

Currently, Cadwyn validates responses against the Python model field names (snake_case), not against the serialized field names (camelCase). This makes it impossible to use Pydantic's built-in camelCase serialization with Cadwyn.

Operating system

Windows 11

Additional context

Related issue found: #109 "Add an ability to modify pydantic schema configuration" (still open)

Root cause analysis:

  • The validation happens after Pydantic's validation_alias (which expects snake_case)
  • But before serialization_alias (which would produce camelCase)
  • This creates a mismatch: Cadwyn validates against the model's Python field names, but the actual response data structure may have already been transformed.

Versions:
Python: 3.13
Cadwyn: 6.2.0
FastAPI: 0.135.3
Pydantic: 2.13.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions