본문 바로가기

Frame Work/FastAPI

FastAPI Form List 파싱: Enum을 함께 쓰면 왜 실패할까

728x90
반응형

 

개요 

FastAPI를 다루다 보면 Pydantic의 BaseModel과 FastAPI에서 제공하는 타입 시스템이 Swagger(OpenAPI) 문서와 자연스럽게 연동되는 점이 큰 장점으로 느껴진다. 그래서 되도록이면 API를 설계할 때도 실제 동작뿐 아니라, Swagger 문서에 스펙이 정확히 드러나도록 코드를 작성하려는 노력을 자주 하게 된다.

 

하지만 이러한 의도를 모두 충족하기에는 FastAPI가 아직 충분히 지원하지 못하는 부분도 존재한다. 그중 하나가 Swagger 상에서 입력 가능한 값이 제한된 필드를 정의하고자 할 때, 이를 Enum + Form + List 조합으로 처리하려는 경우이다.

 

실제로 Enum 값을 Form 필드로 받고, 이를 List 형태로 처리하려고 하면 Swagger에서 생성된 요청과 FastAPI의 파싱 방식 사이에서 예상치 못한 오류가 발생한다. 이 글에서는 Enum 항목을 Form으로 받는 과정과 함께, Form 데이터를 List로 처리하기 위해 시도했던 여러 방법과 그 과정에서 발견한 현실적인 우회 방법(workaround)을 정리해보려 한다.

 

 

Form에 Enum 사용하기

우선 단일값을 처리하는 경우를 살펴보자. 어떤 Form 필드에 특정 값만을 받고자 한다면 Enum과 Form 필드를 활용해 다음과 같은 코드를 작성할 수 있다.

class Label(Enum):
    A:str = "A"
    B: str = "B"

@router.post(
    "/test",
    description="Form 필드",
)
async def form_field_test(
        label: Label = Form()
):
    return BaseResponse.ok(
        data={},
        message_ko="",
        message_en="",
    )

이 경우에 Swagger는 다음과 같은 형태의 API 문서를 생성해 준다.

의도한 대로 Label이라는 Enum 클래스의 값 중에서 특정 값만을 받고자 함을 표현할 수 있게 잘 처리됐다. 이 요청에 대해 다음과 같이 실제 들어온 값과, 타입을 확인해 보자면 Enum으로 잘 처리된 걸 볼 수 있다.

print(label, type(label))

# Result
Label.A <enum 'Label'>

어떤 Form에 대해 단일값으로 입력받고자 할 때는 Enum에 대한 값과 타입을 잘 받는 것을 확인할 수 있다.

 

그러나 문제는 Enum과 값들을 다중(List) 형태로 받고 싶을 때이다.

 

 

Form에 Enum을 List로 처리하면 무슨 문제가 있을까?

이제 Enum으로 명시한 값을 Form List로 받을 때의 동작을 확인하기 위해 코드를 다음과 같이 변경해 보자.

@router.post(
    "/test",
    description="Form 필드",
)
async def form_field_test(
        label: List[Label] = Form()
):
    return BaseResponse.ok(
        data={},
        message_ko="",
        message_en="",
    )

이에 대해 FastAPI가 생성해 준 Swagger 문서는 다음과 같다.

 

List를 명시한 결과 여러 값을 선택할 수 있는 UI로 변경이 되었다. 그러나 두 개의 값을 입력하고 Execute를 누르면 FastAPI에서는 오류가 일어난다.

{
  "error": "Request Validation Error",
  "details": [
    {
      "field": "body.label[0]",
      "type": "enum",
      "message": "Input should be 'A' or 'B'",
      "input": "A,B",
      "expected": ["A", "B"]
    }
  ]
}
 

에러의 내용을 읽어보자면 FastAPI에서는 “A”나 “B”가 입력되는 것을 기대해지만 실제 요청은 [”A, B”]가 들어와서 Error가 일어난 것이다.

 

즉, Enum을 Form List로 받겠다고 선언하면 길이가 1인 배열에 선택한 값을 넣어서 데이터를 생성한다는 것을 알 수 있다.

 

FastAPI에서 FormList 처리에 관한 issue

이 문제는 이미 FastAPI repo에서 논의한 내용이 있었다

https://github.com/fastapi/fastapi/issues/3532?utm_source=chatgpt.com

 

`List[...]` as type for a `Form` field doesn't work as expected · Issue #3532 · fastapi/fastapi

Opening a new issue since no action has been taken on #842 for more than a month. Looks like we have an issue on how fastapi handles lists of elements passed as Form parameter (so anything that is ...

github.com

 

위 issue에서 일어난 논의를 통해 FastAPI에서 FormList를 다룰 때 Error가 일어나는 이유는 다음으로 정리할 수 있었다.

Http Form 데이터는 중복된 키를 List로 표현한다 → l=1, l=2 그러나 Swagger UI가 기본적으로 생성하는 요청은 한 필드에 쉼표로 묶어서 보내는 방식이다. → l=1,2

 

 

추가적인 조사를 통해 알게 된 점은 OpenAPI Specification에는 “explode”라는 개념이 존재한다고 한다. 이는 true와 False 옵션에 따라 다음과 같이 동작한다.

  • explode가 True → 같은 이름을 여러 번 보내는 방식 (Ex, l=1, l=2)
  • explode가 False → 하나의 필드에 쉼표로 나열된 값으로 보냄 (Ex, l=1,2)

 

결론적으로 FastAPI에서 List […]로 타입을 다룰 경우 스키마에 “explode” 옵션을 명시적으로 넣진 않기에 Error가 발생하는 듯 보인다.

 

해당 issue에 달린 댓글 중에서는 explode를 처리하는 예시가 있긴 하지만 이를 매번 적용하고자 한다면 복잡한 부분이라는 생각이 든다. explode 처리 코드를 추가하지 않는 선에서 해결하는 방법을 고민해야 한다.

 

Enum을 FormList로 처리하기

개요에서 언급했지만 이는 어쩌다가 찾아낸 편법이다.

 

Enum을 FormList롤 처리하기 위해서 사용했던 방법은 FastAPI가 FormList를 파싱 할 때 받아들이는 값을 그대로 타입에 명시해 주는 방식이었다.

 

상술했듯 Enum List를 입력하면 FastAPI는 입력값을 다음과 같이 처리한다.

["A","B"]

위 값은 타입 힌트로 보자면 List [str]이다. 이를 그대로 적용하면 될 것 같지만 목표는 Form필드에 허용값을 명시하기 위해 Enum 클래스도 명시해줘야 한다는 부분이다. 그렇다면 다음과 같이 작성할 수 있다.

label: Union[List[Label], List[str]] = Form()

위 코드에 대해 FastAPI는 다음과 같은 Swagger 문서를 생성해 준다.

]

 

이를 코드상에서 print를 찍어보면 다음과 같다.

['A,B'] <class 'list'>

 

마치며

편법으로 다룬 FormList 처리이기 때문에 유효성 검증에 대한 로직도 추가적으로 필요하고 Form List에 대한 타입 힌트를 신경 써줘야 되는 점도 분명 존재한다. 그러나 explode 처리에 대한 코드를 추가하는 것보다야 싸게 먹히는 감이 없지 않은 것 같다.

 

FormList 처리가 FastAPI에서 정식적으로 지원되는 날만을 기다려야 될 듯싶다.

728x90
반응형