Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fastapi pydantic nested models #2864

Closed
9 tasks done
grillazz opened this issue Feb 23, 2021 · 11 comments
Closed
9 tasks done

fastapi pydantic nested models #2864

grillazz opened this issue Feb 23, 2021 · 11 comments
Labels
question Question or problem question-migrate

Comments

@grillazz
Copy link

grillazz commented Feb 23, 2021

First check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.
  • After submitting this, I commit to one of:
    • Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
    • I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
    • Implement a Pull Request for a confirmed bug.

My view models below:

class EnvContainersStatusResponse(BaseModel):
    description: str = Field(...)

    class Config:
        orm_mode = True
        allow_population_by_field_name = True


class EnvContainersResponse(BaseModel):
    container_name: str = Field(alias='_name')
    container_status: EnvContainersStatusResponse

    class Config:
        orm_mode = True
        allow_population_by_field_name = True


class EnvResponse(EnvBase):
    env_id: str = Field(alias='environment_name')
    container_collection: List[EnvContainersResponse] = Field(alias='compute_node_containers')

    class Config:
        orm_mode = True
        allow_population_by_field_name = True

my response is like below

{
  "environment_name": "somenewenv",
  "compute_node_containers": [
    {
      "_name": "compute001",
      "container_status": {
        "description": "online"
      }
    }
  ]
}

question: how i can flatten my response model to have below:

want eliminate another dict and have only key value like "container_status": "online"

{
  "environment_name": "somenewenv",
  "compute_node_containers": [
    {
      "_name": "compute001",
      "container_status": "online"
      
    }
  ]
}
@grillazz grillazz added the question Question or problem label Feb 23, 2021
@ycd
Copy link
Contributor

ycd commented Feb 23, 2021

As far as i know, no. Are there any reasons to have a class for container_status instead of just using container_status: str from your EnvContainersResponse model?

@grillazz
Copy link
Author

grillazz commented Feb 23, 2021

@ycd to take container description i need to follow relation from container to container status as this is how models are designed in legacy codebase

class Container(Base):
    __tablename__ = "container"
...
container_status = relationship("ContainerStatus", lazy='joined')

@paloderosa
Copy link

paloderosa commented Feb 24, 2021

I think this is rather related to pydantic. A discussion has already taken place about flattening (at, pydantic/pydantic#717, for example). Two possible solutions I can think of (and would have to implement in order to understand the details, sorry for the lack of time):

  1. The description attribute in your ORM model is available as container_status.description. You could give that as an alias for the container_status field in your EnvContainersResponse model, but then you would have to subclass the GetterDict class to be able to get nested object attributes and register that new class in the EnvContainersResponse model's Config.
  2. Add to your EnvContainersResponse a validator on container_status field with pre=True that explicitly extracts the subattribute from the ORM object or add a non-public field _container_status that gets the nested object and compute the container_status field from that.

@grillazz
Copy link
Author

grillazz commented Mar 1, 2021

gracias @paloderosa i also fund few suggestions about overwrite presentation for pydatinc. I will try both of solutions you suggested and back with some code examples.

@paloderosa
Copy link

Welcome! (and no issues with having my username changed hahahaha).

@paloderosa
Copy link

@grillazz I actually came up today to the need to implement this functionality in a many-to-many relationship. Here is my implementation of the first proposal. I will assume that you are using SQLAlchemy for ORM, that you are traversing the entities relationships through relationship model attributes and that you have initialized the declarative Base class somewhere.

import functools
from typing import Any, List

from pydantic import BaseModel, Field
from pydantic.utils import GetterDict
from sqlalchemy import Column, String
from sqlalchemy.orm import relationship


# implementation of a recursive getattr
# taken from https://stackoverflow.com/a/31174427
def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return functools.reduce(_getattr, [obj] + attr.split('.'))


class NestedGetterDict(GetterDict):
    def __getitem__(self, key: str) -> Any:
        try:
            return rgetattr(self._obj, key)
        except AttributeError as e:
            raise KeyError(key) from e

    def get(self, key: Any, default: Any = None) -> Any:
        return rgetattr(self._obj, key, default)


class Container(Base):
    __tablename__ = 'container'
    ...
    container_status = relationship("ContainerStatus", lazy='joined')

class ContainerStatus(Base):
    __tablename__ = 'container_status'
    ...
    description = Column(String, nullable=False)


class EnvContainersResponse(BaseModel):
    container_name: str = Field(alias='_name')
    container_status: str = Field(alias='container_status.description')

    class Config:
        orm_mode = True
        allow_population_by_field_name = True
        getter_dict = NestedGetterDict


class EnvResponse(EnvBase):
    env_id: str = Field(alias='environment_name')
    container_collection: List[EnvContainersResponse] = Field(alias='compute_node_containers')

    class Config:
        orm_mode = True
        allow_population_by_field_name = True

This should result in what you are looking for.

@grillazz
Copy link
Author

grillazz commented Mar 3, 2021

sorry for messing your name @paloderosa

@aaronstephenson
Copy link

@grillazz I have a similar need to flatten some nested models, did you get this working?

@paloderosa Thanks for the example you posted! But when I try to implement it, the flattened field name has dot notation (container_status.description). How would I get the field name to be container_status while still having the field value be the nested description value?

@shrutikaponde
Copy link

@aaronstephenson, I solved this by adding response_model_by_alias.

@router.get("/",
            response_model=List[schemas.Command],
            response_model_by_alias=False)

@scd75
Copy link

scd75 commented Apr 15, 2021

hello, building on solution proposed by @paloderosa :

I created this method: get_custom_mapper, that take a dict of key, value such as
{'your_pydantic_custom_name': 'your_model_attribute.nested_attribute'}

def get_custom_mapper(mapping: dict):

    def rgetattr(obj, attr, *args):
        def _getattr(obj, attr):
            return getattr(obj, attr, *args)
        return functools.reduce(_getattr, [obj] + attr.split('.'))

    class CustomGetterDict(GetterDict):
    
        def __getitem__(self, key: str) -> Any:
            if key in mapping:
                try:
                    return rgetattr(self._obj, mapping[key])
                except AttributeError as e:
                    raise KeyError(key) from e
            try:
                return getattr(self._obj, key)
            except AttributeError as e:
                raise KeyError(key) from e

        def get(self, key: Any, default: Any = None) -> Any:
            if key in mapping:
                return rgetattr(self._obj, mapping[key], default)
            return getattr(self._obj, key, default)

    return CustomGetterDict

then you just have to call this in the config class of your pydantic model:

    class Config:
        getter_dict = get_custom_mapper(mapping={
            'your_pydantic_custom_name': 'your_model_attribute.nested_attribute' 
        })

@ben519
Copy link

ben519 commented Dec 26, 2021

I came across this issue as well and found this discussion and @paloderosa's solution very helpful. I ended up documenting the problem and a few other potential solutions here.

@tiangolo tiangolo reopened this Feb 27, 2023
Repository owner locked and limited conversation to collaborators Feb 27, 2023
@tiangolo tiangolo converted this issue into discussion #6869 Feb 27, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

8 participants