2
2

More than 3 years have passed since last update.

Python Fast APIのクエリパラメータや文字列のバリデーションについてまとめてみた

Last updated at Posted at 2021-02-14

Query Parameters and String Validations

  • FastAPIでは、パラメータの追加情報とバリデーションを宣言することができる
from typing import Optional

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
  • これをパラメータのデフォルト値として使用し、パラメータmax_lengthを50に設定することができる
  • デフォルト値 None を Query(None) に置き換える必要があるので、Query の最初のパラメータはデフォルト値を定義するのと同じ目的を果たす

パラメータを必須にする

  • バリデーションやメタデータを宣言する必要がない場合は、デフォルト値を宣言しないだけでqクエリパラメータを必須にすることができる
  • Queryを使用しているときに必要な値を宣言する必要がある場合は、第一引数に...を使用することができる
from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: str = Query(..., min_length=3)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

Path parameters and numeric(数値) validations

  • Queryを使うことでクエリパラメータに対してより細かいバリデーションをかけることができる
  • パスパラメータとして変数を宣言すれば、そのパラメータは常に必須になるが、'...'とともに宣言することで、必須であることを明示することができる ## order the parameters as you need
  • 'q'パラメータをstr型の必須パラメータにしたくて、かつそれ以外の条件を設定したくない時は'Query'を宣言しなくても問題ないが、item_idパラメータにはPathを使う必要がある
  • Pythonは、"default "を持たない値の前に "default "を持つ値を置くと怒られが発生するが、デフォルトのない値(クエリパラメータq)を並べ替えることができる
  • パラメータは名前、型、デフォルトの宣言(Query、Pathなど)で検出されるため、FastAPIでは気にしなくても良い ## Order the parameters as you need, tricks
  • クエリもデフォルト値もないqクエリパラメータと、Pathを使ってpathパラメータのitem_idを宣言して、それらを別の順番にしたい時に使える構文がPythonにある
  • 関数の最初のパラメータとして * を渡すことで、たとえそれらがデフォルト値を持っていなくてもすべてのパラメータがキーワード引数(キーと値のペア)として呼ばれる物として扱ってくれる

Number validations: greater than or equal

  • 文字列同様、数値の制約も宣言することができる

まとめ

  • Query, Pathでは、Query ParametersやString Validationsと同じようにメタデータや文字列バリデーションを宣言することができる

Body - Multiple Parameters

  • Path と Query の使い方を見てきましたが、リクエストボディ宣言のより高度な使い方を見てみましょう。 ## パス、クエリ、ボディパラメータの組み合わせ
  • Fast APIではPath、Query、リクエストボディのパラメータ宣言を自由に混ぜることができる
  • また、デフォルトを None に設定することで、ボディパラメータをオプションとして宣言することもできます。 ## Multiple body parameters
  • 先ほどの例では、パスはアイテム型のJSON bodyを受け付けていた
{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}
  • しかし、複数のボディパラメータ、例えば item と user を宣言することもできます。
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


class User(BaseModel):
    username: str
    full_name: Optional[str] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
    results = {"item_id": item_id, "item": item, "user": user}
    return results
  • この場合、FastAPIは、関数内に複数のボディパラメータ(Pydanticモデルである2つのパラメータ)があることを検知してくれる
  • そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを受け取ることができる
  • FastAPIはリクエストから自動変換を行い、パラメータ項目が特定の内容を受け取るようにします。
  • 複合データの検証を実行し、OpenAPIスキーマと自動ドキュメントのためにそのように文書化します。 ## Singular values in body
  • クエリとパスパラメータのための追加データを定義するためのクエリとパスがあるのと同じように、FastAPI は同等のボディを提供します。
  • 例えば、前のモデルを拡張して、アイテムとユーザー以外にもう一つのキーのimportanceを同じボディに持たせたいと決めることができます。
  • それをそのまま宣言すると、それが単数値であるため、FastAPIはそれがクエリパラメータであると仮定し、FastAPI にBodyを使用して別のボディキーとして扱うように指示することができます。
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


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


class User(BaseModel):
    username: str
    full_name: Optional[str] = None


@app.put("/items/{item_id}")
async def update_item(
    item_id: int, item: Item, user: User, importance: int = Body(...)
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results
# この場合、fastAPIは下記のようなボディを受け取ることができる
{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "importance": 5
}

Multiple body params and query

  • もちろん、必要に応じてボディパラメータに加えてクエリパラメータを追加で宣言することもできます。
  • デフォルトでは、特異値はクエリパラメータとして解釈されるので、明示的にクエリを追加する必要はありません。

単一のボディパラメータを埋め込む

  • PydanticモデルのItemから1つのアイテムのボディパラメータだけを持っているとしましょう。
  • デフォルトでは、FastAPIはそのボディをそのまま受け取ります。
  • しかし、余分なボディパラメータを宣言するときのように、キーアイテムとその中のモデルの内容を持つJSONを期待したい場合は、特別なボディパラメータの埋め込みを使用することができます。
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


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


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

この場合fastAPIはこうではなく

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

このようなボディを扱うことができる

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

まとめ

  • リクエストが単一のボディしか持てない場合でも、パス操作関数に複数のボディパラメータを追加することができる。
  • FastAPIはそれを処理し、関数内の正しいデータを与え、パス操作で正しいスキーマを検証して文書化します。また、ボディの一部として受信する単数値を宣言することもできます。
  • また、単一のパラメータしか宣言されていない場合でも、ボディをキーに埋め込むようにFastAPIに指示することができます。

Body - Fields

  • Query、Path、Bodyを使ってパス操作関数のパラメータに追加のバリデーションやメタデータを宣言できるのと同じように、PydanticのFieldを使ってPydanticモデルの内部でバリデーションやメタデータを宣言することができる。
  • Fieldは他のすべてのもの(Query、Path、Bodyなど)と同様にfastapiからではなく、pydanticから直接インポートされていることに注意してください。
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel, Field

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = Field(
        None, title="The description of the item", max_length=300
    )
    price: float = Field(..., gt=0, description="The price must be greater than zero")
    tax: Optional[float] = None


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

Declare model attributes

  • モデルの属性でFieldを使用することができます。
  • フィールドは、クエリ、パス、ボディと同じように動作し、すべて同じパラメータを持ちます。
  • Field, Query, Bodyなどに付加的な情報を宣言することができます。そして、それは生成されたJSONスキーマに含まれます。
  • 付加情報を追加することについては、後ほどドキュメントで、例を宣言することを学ぶときに詳しく学びます。 ## まとめ
  • PydanticのFieldを使用して、モデル属性のための追加のバリデーションとメタデータを宣言することができます。
  • また、追加のキーワード引数を使用して追加のJSON Schemaメタデータを渡すこともできます。

Body - Nested Models

  • FastAPIを使用すると、pydanticによって任意の深い入れ子になったモデルを定義、検証、文書化、使用することができる ## 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}
    results = results
  • これでタグはそれぞれの項目の型を宣言しているわけではありませんがList型になります。 ## Set Types
  • しかし、よく考えてみると、タグは繰り返しではなく、おそらく一意の文字列になるはずだということに気づきました。
  • そして、Pythonにはユニークなアイテムのセットのための特別なデータ型、セットがあります。
  • そこでSetをインポートして、タグをstrのセットとして宣言することができます。
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
  • これを使えば、たとえデータが重複しているリクエストを受けたとしても、それをユニークな項目のセットに変換して出力してくれます。
  • そして、そのデータを出力するときはいつでも、たとえソースが重複していたとしても、それはユニークなアイテムのセットとして出力されます。
  • また、それに応じて注釈を付けたり、ドキュメントを作成したりします。 ## Nested Models
  • Pydanticモデルの各属性には型があります。
  • しかし、その型自体が別のPydanticモデルになることもあります。したがって、特定の属性名、型、検証を使って、深く入れ子になったJSONの「オブジェクト」を宣言することができる。 ### サブモデルを定義する
  • そして、それを属性の型として使うことができます。
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] = []
    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

特殊な型とバリデーション

  • str, int, float などのような通常の単数型とは別に、より複雑な単数型を使用することができます。strを継承したより複雑な単数型を使用することができる。
  • すべてのオプションを見るには、Pydantic のエキゾチック型のドキュメントをチェックしてください。次の章でいくつかの例を見ることができます。
  • 例えば、Image モデルに url フィールドがあるように、それを str の代わりに Pydantic の HttpUrl と宣言することができます。
  • Pydanticモデルをリストやセットなどのサブタイプとして使うこともできます。
from typing import List, 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()
    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

このような型の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"
        }
    ]
}

 Deeply nested models

  • 深く入れ子にしたモデルを定義することができます。
from typing import List, 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()
    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

Bodies of pure lists

  • 受け取るJSONボディの最上位の値がJSON配列(Pythonのリスト)であれば、Pydanticのモデルと同じように、関数のパラメータで型を宣言することができる。 ## Bodies of arbitrary dicts
  • また、ある型のキーと別の型の値を持つdictとしてボディを宣言することもできます。
  • 有効なフィールド/属性名が何であるかを事前に知る必要がありません(Pydanticモデルの場合のように)。
  • これは、まだ知らないキーを受け取りたい場合に便利です。
  • その他の便利なケースとしては、例えばintのように他の型のキーを持ちたい場合があります。
  • ここではそれを見ていきましょう。
  • この場合、int型のキーを持ち、float型の値を持つものであれば、どのようなdictでも受け付けます。 ## まとめ FastAPIを使用すると、コードをシンプル、短く、エレガントに保ちながら、Pydanticモデルによって提供される最大限の柔軟性を得ることができます。

Schema Extra - Example

  • JSON スキーマに入る付加情報を定義することができます。
  • 一般的な使用例は、ドキュメントに表示される例を追加することです。
  • JSON Schemaの付加情報を宣言する方法はいくつかあります。 ## Pydantic schema_extra
  • Pydanticのドキュメントで説明されているように、Configとschema_extraを使ってPydanticモデルの例を提示することができます。
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

    class Config:
        schema_extra = {
            "example": {
                "name": "Foo",
                "description": "A very nice Item",
                "price": 35.4,
                "tax": 3.2,
            }
        }


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
  • 情報はそのまま出力JSONスキーマに追加されます。 ## Field additional arguments
  • 後述する Field, Path, Query, Body などでは、他の任意の引数を関数に渡すことで JSON スキーマの追加情報を宣言することもできます。
  • 例えば、exampleを追加すると先ほどと同様JSONのスキーマの例を提示することができます。
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()


class Item(BaseModel):
    name: str = Field(..., example="Foo")
    description: Optional[str] = Field(None, example="A very nice Item")
    price: float = Field(..., example=35.4)
    tax: Optional[float] = Field(None, example=3.2)


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results
  • これらの付加的な引数が渡されても、文書化のためのバリデーションは追加されず、注釈のみが追加されることを覚えておいてください。 ## Body additional arguments
  • Field に追加情報を渡すのと同じように、Path、Query、Body なども同じように渡すことができます。
  • 例えば、Body に body リクエストの例を渡すことができます。
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


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


@app.put("/items/{item_id}")
async def update_item(
    item_id: int,
    item: Item = Body(
        ...,
        example={
            "name": "Foo",
            "description": "A very nice Item",
            "price": 35.4,
            "tax": 3.2,
        },
    ),
):
    results = {"item_id": item_id, "item": item}
    return results

Technical Details

  • JSON Schemaは最新バージョンではフィールドのexampleを定義していますが、OpenAPIはexampleを持たない古いバージョンのJSON Schemaをベースにしています。
  • なので、OpenAPIは同じ目的のために独自にexampleを定義していて(exampleではなくexampleとして)、それがdocs UI(Swagger UIを使用)で使用されています。
  • つまり、exampleはJSON Schemaの一部ではありませんが、OpenAPIの一部であり、それがdocs UIで使われることになります。
  • 同じように、フロントエンドのユーザーインターフェースなどをカスタマイズするために、各モデルのJSONスキーマに追加される独自の追加情報を追加することができます。

Extra Data Types

  • これまでは、strやintのような一般的なデータ型を使用していましたが、より複雑なデータ型を使用することもできます。 ## Other data types ここでは、使用できる追加のデータ型をいくつか紹介します。
  • UUID
    • 標準的な "Universally Unique Identifier "で、多くのデータベースやシステムでIDとして一般的に使用されています。
    • リクエストやレスポンスでは str で表されます。
  • datetime.datetime
    • リクエストとレスポンスでは、ISO 8601形式のstrとして表現される ## Example
  • ここでは、上記の型のいくつかを使用したパラメータを持つパス操作の例を示します。
from datetime import datetime, time, timedelta
from typing import Optional
from uuid import UUID

from fastapi import Body, FastAPI

app = FastAPI()


@app.put("/items/{item_id}")
async def read_items(
    item_id: UUID,
    start_datetime: Optional[datetime] = Body(None),
    end_datetime: Optional[datetime] = Body(None),
    repeat_at: Optional[time] = Body(None),
    process_after: Optional[timedelta] = Body(None),
):
    start_process = start_datetime + process_after
    duration = end_datetime - start_process
    return {
        "item_id": item_id,
        "start_datetime": start_datetime,
        "end_datetime": end_datetime,
        "repeat_at": repeat_at,
        "process_after": process_after,
        "start_process": start_process,
        "duration": duration,
    }

Cookie Parameters

  • クッキーパラメータは、クエリやパスパラメータを定義するのと同じ方法で定義することができます。

Header Parameters

  • ヘッダーのパラメータは、クエリ、パス、およびクッキーのパラメータを定義するのと同じ方法で定義できます。 ## Automatic conversion
  • ヘッダーには、Path、Query、Cookie の機能に加えて、ちょっとした追加機能があります。
  • 標準的なヘッダのほとんどは「ハイフン」文字で区切られており、「マイナス記号」(-)としても知られています。しかし、user-agentのような変数はPythonでは無効です。
  • そのため、Headerはデフォルトではパラメータ名の文字をアンダースコア(_)からハイフン(-)に変換してヘッダを抽出して文書化します。
  • また、HTTPヘッダは大文字小文字を区別しないので、Pythonの標準スタイル(別名「snake_case」)で宣言することができます。
  • そのため、最初の文字をUser_Agentなどと大文字にする必要はなく、通常のPythonのコードと同じようにuser_agentを使うことができます。
  • 何らかの理由でアンダースコアからハイフンへの自動変換を無効にしたい場合は、Headerのパラメータconvert_underscoresをFalseに設定してください。 ## Duplicate headers
  • 重複したヘッダを受信することがあります。つまり、同じヘッダに複数の値が含まれている場合です。このような場合は、型宣言のリストを使って定義することができます。
  • 重複したヘッダの全ての値をPythonのリストとして受け取ることになります。
  • 例えば、複数回出現する可能性のあるX-Tokenのヘッダを宣言するには、以下のように書きます。
from typing import List, Optional

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(x_token: Optional[List[str]] = Header(None)):
    return {"X-Token values": x_token}
  • 2つのHTTPヘッダを送信する場合 X-Token: foo X-Token: bar レスポンスはこのようになる { "X-Token values": [ "bar", "foo" ] } ## まとめ
  • ヘッダーを Header で宣言し、Query、Path、Cookie と同じ共通パターンを使用します。
  • また、変数のアンダースコアを心配する必要はありません。

Response Model

  • レスポンスに使用するモデルは、パス操作のいずれかでパラメータ response_model で宣言することができます。
    • @app.get()
    • @app.post()
    • @app.put()
    • @app.delete()
  • これはPydanticモデル属性のために宣言するのと同じ型を受け取りますので、Pydanticモデルであることができますが、例えばList[Item]のようなPydanticモデルのリストであることもできます。
  • FastAPIはこのresponse_modelを使用して次のことを行います。
    • 出力データをその型宣言に変換します。
    • データを検証します。
    • OpenAPI パス操作でレスポンスの JSON スキーマを追加します。
    • 自動ドキュメントシステムで使用されます。
    • 出力データを型にそってバリデーションする ## Return the same input data
  • ここではプレーンテキストでパスワードを含んだUserInモデルを宣言しています。
  • そして、このモデルを使って入力を宣言し、同じモデルを使って出力を宣言しています。
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None


# Don't do this in production!
@app.post("/user/", response_model=UserIn)
async def create_user(user: UserIn):
    return user
  • さて、ブラウザがパスワードでユーザーを作成している場合、APIはレスポンスで同じパスワードを返すようになりました。
  • この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。
  • しかし、同じモデルを別のパス操作に使うと、すべてのクライアントにユーザーのパスワードを送信してしまう可能性があります。 ## Add an output model
  • 代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成することができます。
  • ここでは、パス操作関数がパスワードを含む同じ入力ユーザを返しているにもかかわらず返却値にパスワードが含まれていません
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    return user
  • response_modelに返却して欲しい値の型を宣言すると、その型で返却してくれる
  • FastAPIは、出力モデルで宣言されていないすべてのデータをフィルタリングします ## Response Model encoding parameters
  • レスポンスモデルにデフォルト値を設定することができます。
from typing import List, Optional

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]
  • description: Optional[str] = デフォルトはNone
  • tax: float = デフォルト値として10.5をもつ
  • tags: List[str] = からのListを初期値としてもつ
  • 例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合は、結果からそれらを省略したいことがあります。
  • response_model_exclude_unset=Trueを設定することで、デフォルト値はレスポンスに含まれず、 実際に設定されている値のみがレスポンスに含まれます。
  • つまり、ID foo のアイテムに対してそのパス操作にリクエストを送ると、レスポンスは デフォルト値を含まない { "name": "Foo", "price": 50.2 } となります。
  • しかし、id = barのように、モデルのフィールドにデフォルトの値が付いているデータであれば、結果はレスポンスに含まれます。 ### response_model_include and response_model_exclude
  • response_model_includeとresponse_model_exclude、取得したデータに含まれる(残りの部分を省略する)、または除外する(残りの部分を含む)属性の名前を持つstrのセットを取ります。
  • これは、Pydanticで定義したモデルを1つだけ持っていて、出力からいくつかのデータを削除したい場合の手っ取り早いショートカットとして使うことができます。
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: float = 10.5


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
    "baz": {
        "name": "Baz",
        "description": "There goes my baz",
        "price": 50.2,
        "tax": 10.5,
    },
}


@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
    return items[item_id]


@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
    return items[item_id]

まとめ

  • パス操作デコレータのパラメータ response_model を使用してレスポンスモデルを定義し、特にプライベートデータがフィルタリングされないようにします。
  • 明示的に設定された値のみを返すには、response_model_exclude_unset を使用します。

Extra Models

  • 複数のデータモデルをレスポンスとして扱うことはAPIを利用する上ではよくあることです
  • 例えば、パスワードのデータを入出力に扱う場合には
    • 入力モデルはパスワードを持つ必要があります。
    • 出力モデルはパスワードを持つべきではありません。
    • データベースでは、おそらくパスワードはハッシュ化されている。 ## Multiple models
  • ここでは、モデルのパスワードフィールドと使用場所でモデルがどのように見えるのか、大まかなイメージをご紹介します。

About **user_in.dict()

  • user_inはUserInクラスのPydanticモデルです。Pydanticモデルには.dict()メソッドがあり、モデルのデータを含むdictを返します。そのため、Pydanticのオブジェクトuser_inを作成すると、次のようになります。
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

そして、user_dict = user_in.dict()のように呼び出すとこれで、変数user_dictにデータが入ったdictができました(Pydanticモデルオブジェクトの代わりにdictです)。

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}
  • user_dictのようなdictを取って、user_dictを持つ関数(またはクラス)に渡すと、Pythonはそれを "アンラップ "します。user_dictのキーと値を直接キー-値の引数として渡します。 ``` UserInDB(user_dict) ``` 結果は上と同じです。
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Optional[str] = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

コードの重複を減らす

  • コードの重複を減らすことは、FastAPIのコアアイデアの一つです。コードの重複はバグ、セキュリティ問題、コードの非同期化問題(ある場所では更新しても他の場所では更新しない場合)などの可能性を増加させます。
  • そして、これらのモデルはすべて多くのデータを共有し、属性名や型を重複させていますがもっと良い方法があります。
  • 他のモデルのベースとなる UserBase モデルを宣言し、そのモデルのサブクラスを作成して、そのモデルの属性(型の宣言、検証など)を継承することができます。すべてのデータ変換、検証、ドキュメンテーションなどは通常通りに動作します。
  • このようにして、モデル間の違い(平文パスワードあり、ハッシュドパスワードあり、パスワードなし)だけを宣言することができます。
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str
  • レスポンスを2つの型の連合にすることを宣言できます。これはOpenAPIではanyOf型で定義されます。Pythonの標準的な型のヒントである typing.Unionを使います
  • ユニオンを定義する際には、最も特定の型を最初に含め、その後にあまり特定の型を入れます。以下の例では、Union[PlaneItem, CarItem] の中で、より具体的な PlaneItem が CarItem の前に来ています。 ## まとめ
  • 複数のPydanticモデルを使用し、それぞれのケースに応じてモデルを継承して扱うことができる。
  • そのエンティティが異なる「状態」を持つ必要がある場合は、エンティティごとに単一のデータモデルを持つ必要はない

後日加筆修正して再投稿します。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2