콘텐츠로 이동

본문 - 중첩 모델

FastAPI를 이용하면 (Pydantic 덕분에) 단독으로 깊이 중첩된 모델을 정의, 검증, 문서화하며 사용할 수 있습니다.

리스트 필드

어트리뷰트를 서브타입으로 정의할 수 있습니다. 예를 들어 파이썬 list는:

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: list = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

이는 tags를 항목 리스트로 만듭니다. 각 항목의 타입을 선언하지 않더라도요.

타입 매개변수가 있는 리스트 필드

하지만 파이썬은 내부의 타입이나 "타입 매개변수"를 선언할 수 있는 특정 방법이 있습니다:

typing의 List 임포트

먼저, 파이썬 표준 typing 모듈에서 List를 임포트합니다:

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

타입 매개변수로 List 선언

list, dict, tuple과 같은 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면:

  • typing 모듈에서 임포트
  • 대괄호를 사용하여 "타입 매개변수"로 내부 타입 전달: []
from typing import List

my_list: List[str]

이 모든 것은 타입 선언을 위한 표준 파이썬 문법입니다.

내부 타입을 갖는 모델 어트리뷰트에 대해 동일한 표준 문법을 사용하세요.

마찬가지로 예제에서 tags를 구체적으로 "문자열의 리스트"로 만들 수 있습니다:

from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

집합 타입

그런데 생각해보니 태그는 반복되면 안 돼고, 고유한(Unique) 문자열이어야 할 것 같습니다.

그리고 파이썬은 집합을 위한 특별한 데이터 타입 set이 있습니다.

그렇다면 Set을 임포트 하고 tagsstrset으로 선언할 수 있습니다:

from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

덕분에 중복 데이터가 있는 요청을 수신하더라도 고유한 항목들의 집합으로 변환됩니다.

그리고 해당 데이터를 출력 할 때마다 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다.

또한 그에 따라 주석이 생기고 문서화됩니다.

중첩 모델

Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다.

그런데 해당 타입 자체로 또다른 Pydantic 모델의 타입이 될 수 있습니다.

그러므로 특정한 어트리뷰트의 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다.

모든 것이 단독으로 중첩됩니다.

서브모델 정의

예를 들어, Image 모델을 선언할 수 있습니다:

from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

서브모듈을 타입으로 사용

그리고 어트리뷰트의 타입으로 사용할 수 있습니다:

from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

이는 FastAPI가 다음과 유사한 본문을 기대한다는 것을 의미합니다:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

다시 한번, FastAPI를 사용하여 해당 선언을 함으로써 얻는 것은:

  • 중첩 모델도 편집기 지원(자동완성 등)
  • 데이터 변환
  • 데이터 검증
  • 자동 문서화

특별한 타입과 검증

str, int, float 등과 같은 단일 타입과는 별개로, str을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다.

모든 옵션을 보려면, Pydantic's exotic types 문서를 확인하세요. 다음 장에서 몇가지 예제를 볼 수 있습니다.

예를 들어 Image 모델 안에 url 필드를 str 대신 Pydantic의 HttpUrl로 선언할 수 있습니다:

from typing import Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

이 문자열이 유효한 URL인지 검사하고 JSON 스키마/OpenAPI로 문서화 됩니다.

서브모델 리스트를 갖는 어트리뷰트

list, set 등의 서브타입으로 Pydantic 모델을 사용할 수도 있습니다:

from typing import List, Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    images: Union[List[Image], None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

아래와 같은 JSON 본문으로 예상(변환, 검증, 문서화 등을)합니다:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": [
        "rock",
        "metal",
        "bar"
    ],
    "images": [
        {
            "url": "http://example.com/baz.jpg",
            "name": "The Foo live"
        },
        {
            "url": "http://example.com/dave.jpg",
            "name": "The Baz"
        }
    ]
}

정보

images 키가 어떻게 이미지 객체 리스트를 갖는지 주목하세요.

깊게 중첩된 모델

단독으로 깊게 중첩된 모델을 정의할 수 있습니다:

from typing import List, Set, Union

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    images: Union[List[Image], None] = None


class Offer(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    items: List[Item]


@app.post("/offers/")
async def create_offer(offer: Offer):
    return offer

정보

Offer가 선택사항 Image 리스트를 차례로 갖는 Item 리스트를 어떻게 가지고 있는지 주목하세요

순수 리스트의 본문

예상되는 JSON 본문의 최상위 값이 JSON array(파이썬 list)면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다:

images: List[Image]

이를 아래처럼:

from typing import List

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
    return images

어디서나 편집기 지원

그리고 어디서나 편집기 지원을 받을수 있습니다.

리스트 내부 항목의 경우에도:

Pydantic 모델 대신에 dict를 직접 사용하여 작업할 경우, 이러한 편집기 지원을 받을수 없습니다.

하지만 수신한 딕셔너리가 자동으로 변환되고 출력도 자동으로 JSON으로 변환되므로 걱정할 필요는 없습니다.

단독 dict의 본문

일부 타입의 키와 다른 타입의 값을 사용하여 dict로 본문을 선언할 수 있습니다.

(Pydantic을 사용한 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 알 필요가 없습니다.

아직 모르는 키를 받으려는 경우 유용합니다.


다른 유용한 경우는 다른 타입의 키를 가질 때입니다. 예. int.

여기서 그 경우를 볼 것입니다.

이 경우, float 값을 가진 int 키가 있는 모든 dict를 받아들입니다:

from typing import Dict

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

JSON은 오직 str형 키만 지원한다는 것을 염두에 두세요.

하지만 Pydantic은 자동 데이터 변환이 있습니다.

즉, API 클라이언트가 문자열을 키로 보내더라도 해당 문자열이 순수한 정수를 포함하는한 Pydantic은 이를 변환하고 검증합니다.

그러므로 weights로 받은 dict는 실제로 int 키와 float 값을 가집니다.

요약

FastAPI를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧게, 그리고 우아하게 유지할 수 있습니다.

물론 아래의 이점도 있습니다:

  • 편집기 지원 (자동완성이 어디서나!)
  • 데이터 변환 (일명 파싱/직렬화)
  • 데이터 검증
  • 스키마 문서화
  • 자동 문서