DRFのシリアライザとto_internal_value()


こんにちは!開発部の米森です! 現在のプロジェクトでDjango Restframework(以下DRF)を使用してWeb APIを実装しています。とても便利なフレームワークなので効率的に開発を進めることができるのですが、DRF固有の概念であるシリアライザーにハマってしまったので、そのあたりの情報共有をさせてください!

DRF・シリアライザーとは

Django Rest Framework(DRF)は、DjangoをベースにしたWeb APIフレームワークです。その中でもシリアライザーは、下記のような役割を担う重要なコンポーネントです。
  1. データ変換:モデルインスタンスをJSONなどに変換し、またその逆も行う
  2. バリデーション:データが正しい形式であるかをチェックし、不正なデータが保存されるのを防ぐ
これらを効率的に実装するための便利なクラスやメソッドが提供されているとても便利です。しかし一方で、バリデーションが実行される順番など、実装する上で気を付けて置くべきことがいくつかあり、ハマってしまった経験があります…

シリアライザーでのバリデーション

DRFのシリアライザでバリデーションを実装する場合、下記の選択肢があります。
  • ①モデル or シリアライザー宣言時にバリデーターを指定する
  • ②シリアライザークラスのvalidate_<field>でフィールド単位で検証する
  • ③シリアライザークラスのvalidate()でオブジェクト単位で検証する

実際に実装してみる

上記のバリデーションがどのように走っているのかを見るために、簡単な例を実装してみます。今回は「上限値」と「下限値」がクライアントからPOSTされ、それをDBに保存するというケース想定します。

モデル

まずはmodels.py にモデルを定義。モデルとはDBとのやり取りを管理するためのクラスです。モデルクラスがDBのテーブル、フィールドがテーブルのカラム、インスタンスがレコードに相当します。 クライアントから「上限値」と「下限値」の2つがPOSTされるので、それ用のモデルを作ってあげます。

シリアライザ

次に、本題であるシリアライザクラスを定義します。DRFのModelSerializerを継承することで、先ほど定義したモデル専用のシリアライザクラスを簡単に定義できます。

ビュー

クライアントからのリクエストを受け取って、シリアライザを通してモデル経由でDBに保存するというロジックを作ります。ModelViewSetを使用するとモデルに対するCRUD操作を簡単に実装できます。 先ほど実装したシリアライザをserializer_classで指定すると、クライアントからPOSTされたパラメータがそのシリアライザを通ってバリデーションされるようになります。シリアライザ側で model = TestModel とモデルを指定してあげたので、バリデーション後のポストパラメータはモデルに行き、DBにレコードどして保存されるという流れです。

URL

最後に、URLルーティングをして↑のビューをAPIで叩けるようにします。

実際にPOSTしてみる

実装は済んだので、/test/に対して最小値と最大値をPOSTしてみます。VS CodeにはREST CLIENTという便利なAPIインターフェース拡張機能があるので、それを使用して動作確認します。 先ほどルーティングしたURLに対して、”bottom_limit”と”upper_limit”をPOSTします。すると下記の通り201レスポンスが返却されました。
バリデーション自体は通っているようです。値がDBに登録されているか、API経由で確認してみます。ModelViewSetを使えば自動的にHTTPメソッドに応じたCRUD操作を内部的に行ってくれるので、先ほどと同じURLに対してGETメソッドでリクエストを送信すれば、一覧データを取得してくれます。 上記APIを実行すると、ステータスコード200で、先ほど登録した上限値・下限値がデータに含まれたレスポンスが返ってきます。問題なくDBに登録されているようです。

シリアライザバリデーションを実装してみる

では、本題であるシリアライザバリデーションを実装してみます。DRFのシリアライザでバリデーションを実装する場合、下記の選択肢があります。
  • ①モデルのフィールド定義時にバリデーターを指定する
  • ②シリアライザークラスのvalidate_<field>でフィールド単位で検証する
  • ③シリアライザークラスのvalidate()でオブジェクト単位で検証する
注意すべきなのは、これら3つのバリデーションは上記の通りに実行されるということです。

①モデルにバリデーターを指定する

まずはフィールドに定義するバリデータを作ります。 挙動を確認するだけなので、デバッグプリントのみの実装です。これをモデルで定義しているフィールドに追加します。

②validate_[field]でフィールド単位で検証する

validate_[field]() メソッドは、特定のフィールドに対するカスタムバリデーションを行うためのメソッドです。validate_<モデルで定義したフィールド名>()というメソッドを追加すると、シリアライザクラスのバリデーション実行時に自動で呼び出されます。例えばvalidate_bottom_limit()とすると、POSTされてきたパラメータの”bottom_limit”を自動で引数に受け取り、値を検証することができます。

③シリアライザークラスのvalidate()でオブジェクト単位で検証する

validate()メソッドは、シリアライザ全体に対するバリデーションを行うためのメソッドです。このメソッドを使用することで、複数のフィールドにまたがるバリデーションロジックを実装できます。引数dataにはクライアントからのポストパラメータがdict形式で格納されているので、任意のパラメータを取り出して検証できます。今回の場合だと、上限値と下限値を取り出してその大小関係を比較する、のようなケースが考えられます。

バリデーションの順番を見てみる

3種類のバリデーションを実装しました。上述の通り、バリデータ ⇒ validate_<field>()validate() という順番で実行されるはずなので、実際にパラメータをPOSTしてログを見てみましょう。先ほどと同じデータをPOSTしてみます。 すると下記のようなログが出力されました。想定通りの順番で実行されています。

to_internal_value()でパラメータの調整

to_internal_value()は、シリアライザクラスに渡されたパラメータを、バリデーションが始まる前に変換するための関数です。クライアントからPOSTされるパラメータがモデルで定義している型・フォーマットでない場合、そのままだとバリデーションで弾かれてしまいますが、この関数で調整をしてあげることでバリデーションが通るようになります。 ではシリアライザクラスに追加してみます。こちらもデバッグプリントをするだけのメソッドにします。 これで再度POSTしてみると下記の通り、バリデーションが始まる前にこの関数が呼ばれていることが分かります。

実際のユースケース

今のモデルは、「最大値」と「最小値」が別のフィールドとして定義されていますが、クライアント側からは"<最小値>-<最大値>"のように1つの文字列データからPOSTされるというケースを想定します。今の実装のままだと、当然エラーになってしまいます。 下記の通り、モデルで定義されているupper_limitbottom_limit無いのでBadRequestが返ってきてしまいました。
この時のログを見ると、to_internal_value()までは到達していることが分かります。 なので、to_internal_value()の中でデータの変換ロジックを実装しましょう。 これで “limit” が “upper_limit”と “bottom_limit”の2つに分割されるはずなので、再度”limit”をPOSTします。
201レスポンスが返ってきました。ログにも全てのバリデーションログが出力されています。

to_internal_value()の注意点

一件便利そうな関数ですが、注意点が1つ。to_internal_value()はシリアライザクラスのベースクラスであるSerializerで定義されているメソッドです。そのため、子クラスであるTestSerializerto_internal_value()を定義すると、ベースクラスのメソッドがオーバーライドされてしまいます。従って、子クラス内でsuper().to_internal_value()を呼び出さないと、想定外の挙動を引き起こす可能性があります。 試しに、親クラスの該当メソッドを呼び出さない実装にしてみます。 この状態でAPIを実行すると、登録は問題なくされるのですが、ログを見ると、①のバリデータと②のフィールド検証が走っていないことが分かります。
親クラスであるSerializerの該当メソッドを見てみると、この関数内で①バリデータでの検証と②フィールドでの検証を実行していることが分かります。 従って、このクラスを継承した子クラスを宣言して、to_internal_value()を実装する場合は、その中で明示的にsuper().to_internal_value()を呼び出さないと、本来実行されるはずのバリデーションが行われず、意図せず不正データがデータベースに保存される可能性があります。

まとめ

このあたりの仕様をあまり詳しく調べずに実装を進めていたせいで、ハマった経験があったので今回共有させていただきました。フレームワークやライブラリは便利な反面、その仕様を理解しておかないと想定外の動きを見せるので、公式ドキュメントやソースコードを見ることの大切さを改めて痛感しました。

エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!