본문으로 바로가기
728x90
반응형

목차

     

     

    개요 

    FastAPI의 공식문서에는 pydantic을 이용한 예제들이 많이 등장한다. 하지만 pydantic보다 dataclass를 선호해서 FastAPI+dataclass 조합을 이용중인데 이대로 사용하다 보니 Swagger 문서에서 Response 되는 Data를 명시하는 부분에 제외(exclude)할 데이터를 설정하는 부분에서 문제가 있었다.

     

    이 문제를 해결하기위해 python dataclass를 pydantic의 BaseModel로 변환시켜 보자는 아이디어를 떠올렸고 이번 포스팅은 그 방법을 기록한 내용이다.

     

     

    1. FastAPI의 response_model_exclude ?

    FastAPI의 APIRouter 클래스에는 다음과 같은 옵션들이 존재한다.

    이 중에서도 response_model_exclude라는 옵션이 존재하는데 이는 Response Data에서 특정 필드를 제외하는 옵션이다. pydantic의 BaseModel을 이용한다면 해당 옵션이 동작하지만 필자의 경우 dataclass를 이용하고 있기 때문에 해당 옵션을 사용하지 못한다는 문제가 있었다.

     

    2. pydantic의 create_model을 이용해 dataclass를 BaseModel로 변환하기

    결국 response_model_exclude를 사용하려면 pydantic의 BaseModel을 써야 하기에 dataclass를 pydantic의 BaseModel로 변환하는 방법이 없을까를 고민하게 됐다. 조사해 보니 pydantic의 create_model이라는 함수가 있는 걸 확인했다.

    https://docs.pydantic.dev/latest/api/base_model/#pydantic.create_model

    pydantic의 공식문서에는 해당 기능이 Dynamic 한 BaseModel을 만들 수 있다고 설명하고 있다. 조금의 삽질 끝에 다음과 같이 간단한 helper 함수를 만드는 방식으로 create_model을 이용하여 dataclass를 BaseModel로 변환할 수 있었다.

    from pydantic import Bcreate_model
    from typing import get_type_hints
    
    def dataclass_to_basemodel(entity):
    
        hints = get_type_hints(entity)
        new_model = create_model("DynamicBaseModel", **{k: (v, ...) for k, v in hints.items()})
        
        return new_model

    그러나 위와 같이 사용할 경우 APIRouter의 response_model_exclude 옵션이 제대로 동작하지 않았다.

     

    3. pydantic의 create_model을 만들 때 get_type_hints와 Field 사용하기

    앞서 간단히 만들었던 helper 함수의 문제를 개선해 보자. 앞서 기재한 함수의 문제는 동적으로 생성한 BaseModel에 대해 APIRouter의 response_model_exclude 옵션이 동작하지 않는다는 점이다. 그렇다면 BaseModel을 생성할 때 특정 필드를 exclude 시킬 수 있는 방법도 있을 것 같다.

     

    이를 가능하게 하는 방법은 pydantic의 Field 객체를 이용하는 것이었다.

    from pydantic import Field
    ...
    
    hints = get_type_hints(entity)
    
    attributes = {}
    for k, v in hints.items():
        if k in exclude_fields:
            attributes[k] = (v, Field(..., exclude=True))
        else:
            attributes[k] = (v, Field(..., include=True))
    
    new_model = create_model("DynamicBaseModel", **attributes)

     

    typing 모듈의 get_type_hints에 dataclass를 넣어주면 해당 dataclass의 field가 어떤 타입을 가지고 있는지 dictionary 형태로 추출해준다. 그러고 난 다음 pydantic의 Field를 이용해 Swagger 문서에서 제외할 데이터에 exclude옵션을 설정해주자. 

     

    그렇게 만들어낸 필드를 다시 create_model을 사용하여 Dynamic한 BaseModel을 만들 수 있다.

     

    4. 마치며

    dataclass를 이용하여 ResponseModel을 표현하는 방법에 대해서는 오랜 시간 고민한 문제이다.

     

    이 조사를 하기 이전에는 typing과 Generic을 적절히 이용해 클래스를 만들고 데이터 구조를 추상화하여 type hinting을 통해  해결했었지만 이렇게 한 경우 데이터 구조대로 클래스를 만들어야 했기에 번거로움이 수반됐다.

     

    이번에 사용한 방법은 이미 만들어둔 모델을 통해 제어하는 방법이기에 비교적 수월했지만 FastAPI는 pydantic과 연계하여 구조를 잡아놨기에 그 틀을 벗어나 Framework를 사용하는 것에 어떤 의미가 있을지는 고민해봐야겠다.

     

     

    5.  Reference

     

    FastAPI Response Model

    - https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude 

     

    Response Model - Return Type - FastAPI

    FastAPI framework, high performance, easy to learn, fast to code, ready for production

    fastapi.tiangolo.com

     

    Pydantic Dynamic Create Base Model - create_model function

    - https://docs.pydantic.dev/latest/api/base_model/#pydantic.create_model 

     

    BaseModel - Pydantic

    Dynamically creates and returns a new Pydantic model, in other words, create_model dynamically creates a subclass of BaseModel. Parameters: Name Type Description Default __model_name str The name of the newly created model. required __config__ ConfigDict |

    docs.pydantic.dev

     

    StackOverFlow - pydantic create_model usage

    - https://stackoverflow.com/questions/65888153/creating-a-pydantic-model-dynamically-from-a-python-dataclass 

     

    Creating a Pydantic model dynamically from a Python dataclass

    I'd like to dynamically create a Pydantic model from a dataclass, similar to how you can dynamically create a Marshmallow schema from a dataclass as in marshmallow-dataclass or https://stevenloria....

    stackoverflow.com

     

    Blog - dataclass to pydantic basemodel conversion

    - https://www.zachbellay.com/posts/dataclass-to-pydantic-basemodel-conversion/ 

     

    Zach Bellay | Dataclass to Pydantic BaseModel Conversion

    3 min, 532 words Note: This post is a dramaticized development experience I had at work that stemmed largely from miscommunication and bad assumptions. Nonetheless, if you're in a situation like this, here's what I did. The Problem Let's say an existing co

    www.zachbellay.com

     

    728x90
    반응형