Skip to content

10. Body - Nested Models


1. Body - Nested Models

  • FastAPI를 사용하면 임의로 nested 모델을 정의, 검증 및 문서화할 수 있다.


2. List fields

  • 속성을 하위 타입으로 정의할 수 있다.


  • 예를 들어, 다음과 같이 Python list를 작성할 수 있다.


from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = 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는 항목들이 존재하는 리스트로서 사용될 수 있다.


3. List fields with type parameter

  • 하지만 위의 예제와 같이 작성하면, 타입에 대한 모호성이 발생할 수 있다.
  • 이를 해결하기 위해 Python에는 "내부 타입" 또는 "타입 매개변수"가 있는 리스트를 선언하는 방법을 사용한다.


1) Import typing's List

  • 먼저, 표준 Python typing 모듈의 List를 임포트한다.


from typing import Optional, List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = 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


2) Declare a List with a type parameter

  • list, dict, tuple과 같은 내부 타입을 포함하는 타입은 다음과 같이 선언한다.


1] typing 모듈에서 임포트한다.

2] 대괄호 []를 사용하여 내부 타입으로 전달한다.


  • 다음과 같이 작성할 수 있다.


from typing import List

my_list: List[str]


  • 위의 예제처럼 하는 것이 타입 선언에 대한 표준 Python 구문이다.
  • 내부 타입이 있는 모델 속성에 대해 동일하게 사용하면 된다.


  • 따라서 다음과 같이 문자열 타입에 대한 리스트 tags를 만들 수 있다.


from typing import Optional, List
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = 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


4. Set types

  • tags라는 것이 가지고 있는 의미를 생각해보면, tags 안에 있는 항목들은 중복되어서는 안된다.
  • 그러므로 List가 아니라 중복되지 않는 집합인 Set을 사용해야 한다.


  • 다음과 같이 Set을 임포트한 후 tagsstr의 집합으로 선언할 수 있다.


from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = 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


  • 위의 예제와 같이 작성하면, 중복 데이터가 포함된 request를 수신하더라도 중복되지 않는 집합으로 변환된다.
  • 이와 마찬가지로 해당 데이터를 출력할 때마다 소스가 중복되더라도 고유한 항목의 집합으로 출력된다.


5. Nested Models

  • Pydantic 모델의 각 속성은 타입을 가지고 있다.
  • 하지만 그 타입 자체가 또 다른 Pydantic 모델이 될 수 있다.
  • 따라서 특정 속성 이름, 타입 및 유효성 검사를 사용하여 nested JSON "객체"를 선언할 수 있다.


1) Define a submodel

  • Image 모델을 정의해 보자.


from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

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

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

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

    return results


2) Use the submodel as a type

  • 그런 다음 속성의 타입으로 사용할 수 있다.


from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

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

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = set()
    image: Optional[Image] = 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는 다음과 같은 body를 예상하게 된다.


{
  "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"
  }
}


6. Special types and validation

  • str, int, float 등과 같은 일반적인 단일 타입과는 별도로 str에서 상속되는 더 복잡한 단일 타입을 사용할 수 있다.


  • 예를 들어, 다음과 같이 Image 모델에서 url 필드의 str 타입 대신 Pydantic의 HttpUrl 타입으로 선언할 수 있다.


from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()

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

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = set()
    image: Optional[Image] = 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인지 확인하게 된다.


7. Attributes with lists of submodels

  • Pydantic 모델을 list, set 등의 하위 타입으로 사용할 수도 있다.


from typing import Optional, Set, List
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()

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

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = set()
    images: Optional[List[Image]] = 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는 다음과 같은 body를 예상하게 된다.


{
  "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"
    }
  ]
}


8. Deeply nested models

  • deeply nested 모델을 정의할 수도 있다.


from typing import Optional, Set, List
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()

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

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

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

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


9. Bodies of pure lists

  • 예상하는 JSON body의 최상위 값이 JSON array(Python 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


10. Bodies of arbitrary dicts

  • 특정 타입의 키와 특정 타입의 값을 사용하여 body를 dict로 선언할 수 있다.
  • 이런 경우 유효한 필드/속성 이름이 무엇인지 미리 알 필요 없거나 아직 모르는 키를 수신하려는 경우에 유용하다.


  • 다음의 예제는 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


Note

  • 원래 JSON은 키의 타입으로 str 타입만 지원한다.
  • 하지만 Pydantic의 자동 데이터 변환으로 인해, 클라이언트가 보낸 키에 str 타입이 아닌 타입이 포함되어 있으면 Pydantic이 자동으로 변환하고 유효성을 검사한다.

References