sqlalchemyfastapipydantic

Pydantic serialization - return a flat response from nested models


I have a model, called Candidate that I'm trying to return, two of the fields refer to another model, StaffMember. These are originally coming from my database/ORM as sqlalchemy responses.

class StaffMember(BaseModel):
    fname: str
    sname: str

    @computed_field
    @property
    def full_name(self) -> str:
        return f"{self.fname} {self.sname}"

    model_config = ConfigDict(from_attributes=True)

class CandidateResponse(BaseModel):
    id: int
    fname: str
    sname: str
    dob: date
    created_on: datetime
    created_by: StaffMember
    updated_on: datetime
    updated_by: StaffMember

    model_config = ConfigDict(from_attributes=True)

I want to return a CandidateResponse dict/JSON as a single-level. Normally, the response looks like this:

{
  "id": 2,
  "fname": "Joe01",
  "sname": "Bloggs",
  "dob": "2004-12-31",
  "created_on": "2025-02-17T09:34:27",
  "created_by": {
    "fname": "System",
    "sname": "Default",
    "full_name": "System Default"
  },
  "updated_on": "2025-02-17T09:34:27",
  "updated_by": {
    "fname": "System",
    "sname": "Default",
    "full_name": "System Default"
  }
}

However, I don't want any nested fields. Is there a way to do this? - ideally without having to define all the fields again.

I'm looking for a response more like:

{
  "id": 2,
  "fname": "Joe01",
  "sname": "Bloggs",
  "dob": "2004-12-31",
  "created_on": "2025-02-17T09:34:27",
  "created_by_fname": "System",
  "created_by_sname": "Default",
  "created_by_full_name": "System Default",
  "updated_on": "2025-02-17T09:34:27",
  "updated_by_fname": "System",
  "updated_by_sname": "Default",
  "updated_by_full_name": "System Default"
}

I could use field_serializer but I feel like that is a messy solution as I would multiple computed fields (per nested model) to create.

I would also ask:

  1. Is this an appropriate way to do it? - the API consumer is expecting a flat object (not with nesting)
  2. Is there another approach that I should consider? - I've focused on this, but maybe I'm looking at fixing/changing the wrong thing.

Solution

  • You can use @model_serializer to achieve this:

    from collections.abc import Generator
    from typing import Any
    
    from pydantic import BaseModel, model_serializer
    
    
    class FooModel(BaseModel):
        subkey: str
    
    
    class BarModel(BaseModel):
        key: FooModel
        otherkey: int
    
        @model_serializer()
        def serialize_model(self):
            return {k: v for k, v in model_iter(self)}
    
    
    def model_iter(model: BaseModel) -> Generator[tuple[str, Any], None, None]:
        for field, value in model:
            if isinstance(value, BaseModel):
                for key, val in model_iter(value):
                    yield f"{field}_{key}", val
            else:
                yield field, value
    
    
    bar = BarModel(otherkey=1, key=FooModel(subkey="hello"))
    
    print(bar.model_dump()) # {'key_subkey': 'hello', 'otherkey': 1}
    

    This will work as a FastAPI response model, but will still generate an unnested JSON schema in the OpenAPI documentation, but that can probably be achieved with some extra effort, if required.