こんにちは!開発部の米森です! 現在のプロジェクトでDjango Restframework(以下DRF)を使用してWeb APIを実装しています。とても便利なフレームワークなので効率的に開発を進めることができるのですが、DRF固有の概念であるシリアライザーにハマってしまったので、そのあたりの情報共有をさせてください!
DRF・シリアライザーとは
Django Rest Framework(DRF)は、DjangoをベースにしたWeb APIフレームワークです。その中でもシリアライザーは、下記のような役割を担う重要なコンポーネントです。- データ変換:モデルインスタンスをJSONなどに変換し、またその逆も行う
- バリデーション:データが正しい形式であるかをチェックし、不正なデータが保存されるのを防ぐ
シリアライザーでのバリデーション
DRFのシリアライザでバリデーションを実装する場合、下記の選択肢があります。- ①モデル or シリアライザー宣言時にバリデーターを指定する
- ②シリアライザークラスの
validate_<field>
でフィールド単位で検証する - ③シリアライザークラスの
validate()
でオブジェクト単位で検証する
実際に実装してみる
上記のバリデーションがどのように走っているのかを見るために、簡単な例を実装してみます。今回は「上限値」と「下限値」がクライアントからPOSTされ、それをDBに保存するというケース想定します。モデル
まずはmodels.py
にモデルを定義。モデルとはDBとのやり取りを管理するためのクラスです。モデルクラスがDBのテーブル、フィールドがテーブルのカラム、インスタンスがレコードに相当します。
クライアントから「上限値」と「下限値」の2つがPOSTされるので、それ用のモデルを作ってあげます。
1 2 3 4 5 |
# models.py class TestModel(models.Model): '''動作確認用テストモデル''' bottom_limit = models.IntegerField('下限値') upper_limit = models.IntegerField('上限値') |
シリアライザ
次に、本題であるシリアライザクラスを定義します。DRFのModelSerializerを継承することで、先ほど定義したモデル専用のシリアライザクラスを簡単に定義できます。
1 2 3 4 5 6 |
# serializers.py class TestSerializer(serializers.ModelSerializer): '''動作確認用テストシリアライザ''' class Meta: model = TestModel # -> 先ほど定義したモデルを指定 fields = ['bottom_limit', 'upper_limit'] # -> バリデーション対象のフィールドを指定 |
ビュー
クライアントからのリクエストを受け取って、シリアライザを通してモデル経由でDBに保存するというロジックを作ります。ModelViewSet
を使用するとモデルに対するCRUD操作を簡単に実装できます。
1 2 3 4 |
# views.py class TestViewSet(viewsets.ModelViewSet): '''動作確認用テストビュー''' serializer_class = TestSerializer # -> 先ほどのシリアライザ指定 |
serializer_class
で指定すると、クライアントからPOSTされたパラメータがそのシリアライザを通ってバリデーションされるようになります。シリアライザ側で model = TestModel
とモデルを指定してあげたので、バリデーション後のポストパラメータはモデルに行き、DBにレコードどして保存されるという流れです。
URL
最後に、URLルーティングをして↑のビューをAPIで叩けるようにします。
1 2 3 4 5 6 7 |
# urls.py router = routers.DefaultRouter() router.register('test', TestViewSet) # -> パスとビューを指定 urlpatterns = [ path('', include(router.urls)), # -> URLパターンに含める ] |
実際にPOSTしてみる
実装は済んだので、/test/
に対して最小値と最大値をPOSTしてみます。VS CodeにはREST CLIENTという便利なAPIインターフェース拡張機能があるので、それを使用して動作確認します。
1 2 3 4 5 6 7 8 |
### 上限値・下限値登録テスト POST http://localhost:8000/test/ Content-Type: application/json { "bottom_limit": 1, "upper_limit": 10 } |
ModelViewSet
を使えば自動的にHTTPメソッドに応じたCRUD操作を内部的に行ってくれるので、先ほどと同じURLに対してGETメソッドでリクエストを送信すれば、一覧データを取得してくれます。
1 2 3 |
### 上限値・下限値一覧テスト GET http://localhost:8000/test/ Content-Type: application/json |
シリアライザバリデーションを実装してみる
では、本題であるシリアライザバリデーションを実装してみます。DRFのシリアライザでバリデーションを実装する場合、下記の選択肢があります。- ①モデルのフィールド定義時にバリデーターを指定する
- ②シリアライザークラスの
validate_<field>
でフィールド単位で検証する - ③シリアライザークラスの
validate()
でオブジェクト単位で検証する
①モデルにバリデーターを指定する
まずはフィールドに定義するバリデータを作ります。
1 2 3 4 |
# validators.py class TestValidator: def __call__(self, value): print('① --- TestValidator: バリデーターでの検証') |
1 2 3 4 5 |
# models.py class TestModel(models.Model): '''動作確認用テストモデル''' bottom_limit = models.IntegerField('下限値', validators=[TestValidator()]) # -> バリデータを追加 upper_limit = models.IntegerField('上限値', validators=[TestValidator()]) # -> バリデータを追加 |
②validate_[field]でフィールド単位で検証する
validate_[field]()
メソッドは、特定のフィールドに対するカスタムバリデーションを行うためのメソッドです。validate_<モデルで定義したフィールド名>()
というメソッドを追加すると、シリアライザクラスのバリデーション実行時に自動で呼び出されます。例えばvalidate_bottom_limit()
とすると、POSTされてきたパラメータの”bottom_limit”を自動で引数に受け取り、値を検証することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# serializers.py class TestSerializer(serializers.ModelSerializer): '''動作確認用テストシリアライザ''' class Meta: model = TestModel fields = ['bottom_limit', 'upper_limit'] # 新規追加 def validate_bottom_limit(self, value): print('② --- validate_bottom_limit(): 下限値のフィールド検証') # valueを検証し、問題があれば例外処理を行う return value # 新規追加 def validate_upper_limit(self, value): print('② --- validate_upper_limit(): 上限値のフィールド検証') # valueを検証し、問題があれば例外処理を行う return value |
③シリアライザークラスのvalidate()でオブジェクト単位で検証する
validate()
メソッドは、シリアライザ全体に対するバリデーションを行うためのメソッドです。このメソッドを使用することで、複数のフィールドにまたがるバリデーションロジックを実装できます。引数data
にはクライアントからのポストパラメータがdict形式で格納されているので、任意のパラメータを取り出して検証できます。今回の場合だと、上限値と下限値を取り出してその大小関係を比較する、のようなケースが考えられます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# serializers.py class TestSerializer(serializers.ModelSerializer): '''動作確認用テストシリアライザ''' class Meta: model = TestModel fields = ['bottom_limit', 'upper_limit'] def validate_bottom_limit(self, value): print('② --- validate_bottom_limit(): 下限値のフィールド検証') return value def validate_upper_limit(self, value): print('② --- validate_upper_limit(): 上限値のフィールドaa検証') return value # 新規追加 def validate(self, data): print('③ --- validate(): 下限値・上限値の複合検証') # 例: 上限値と下限値を取り出して、大小関係を比較する return data |
バリデーションの順番を見てみる
3種類のバリデーションを実装しました。上述の通り、バリデータ ⇒validate_<field>()
⇒ validate()
という順番で実行されるはずなので、実際にパラメータをPOSTしてログを見てみましょう。先ほどと同じデータをPOSTしてみます。
1 2 3 4 5 6 7 8 9 |
### # 上限値・下限値テスト POST http://localhost:8000/test/ Content-Type: application/json { "bottom_limit": 1, "upper_limit": 10 } |
1 2 3 4 5 |
① --- TestValidator: バリデータでの検証 ② --- validate_upper_limit(): 上限値のフィールド検証 ① --- TestValidator: バリデータでの検証 ② --- validate_bottom_limit(): 下限値のフィールド検証 ③ --- validate(): 下限値・上限値の複合検証 |
to_internal_value()でパラメータの調整
to_internal_value()
は、シリアライザクラスに渡されたパラメータを、バリデーションが始まる前に変換するための関数です。クライアントからPOSTされるパラメータがモデルで定義している型・フォーマットでない場合、そのままだとバリデーションで弾かれてしまいますが、この関数で調整をしてあげることでバリデーションが通るようになります。
ではシリアライザクラスに追加してみます。こちらもデバッグプリントをするだけのメソッドにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# serializers.py class TestSerializer(serializers.ModelSerializer): '''動作確認用テストシリアライザ''' class Meta: model = TestModel fields = ['bottom_limit', 'upper_limit'] # 新規追加 def to_internal_value(self, data): print('★ --- to_internal_value(): 検証前の調整') return super().to_internal_value(data) def validate_bottom_limit(self, value): print('② --- validate_bottom_limit(): 下限値のフィールド検証') return value def validate_upper_limit(self, value): print('② --- validate_upper_limit(): 上限値のフィールドaa検証') return value def validate(self, data): print('③ --- validate(): 下限値・上限値の複合検証') return data |
1 2 3 4 5 6 |
★ --- to_internal_value(): 検証前の調整 ① --- TestValidator: バリデータでの検証 ② --- validate_upper_limit(): 上限値のフィールド検証 ① --- TestValidator: バリデータでの検証 ② --- validate_bottom_limit(): 下限値のフィールド検証 ③ --- validate(): 下限値・上限値の複合検証 |
実際のユースケース
今のモデルは、「最大値」と「最小値」が別のフィールドとして定義されていますが、クライアント側からは"<最小値>-<最大値>"
のように1つの文字列データからPOSTされるというケースを想定します。今の実装のままだと、当然エラーになってしまいます。
1 2 3 4 5 6 7 |
### 上限値・下限値テスト '1-10' という1つの文字列データを送る POST http://localhost:8000/test/ Content-Type: application/json { "limit": "5-10" } |
upper_limit
がbottom_limit
無いのでBadRequestが返ってきてしまいました。
この時のログを見ると、to_internal_value()
までは到達していることが分かります。
1 2 |
★ --- to_internal_value(): 検証前の調整 Bad Request: /test/ |
to_internal_value()
の中でデータの変換ロジックを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# serializers.py class TestSerializer(serializers.ModelSerializer): '''動作確認用テストシリアライザ''' class Meta: model = TestModel fields = ['bottom_limit', 'upper_limit'] def to_internal_value(self, data): print('★ --- to_internal_value(): 検証前の調整') # ロジックを追加 new_data = data.copy() limit = new_data.pop('limit') if not limit: # limitが空の場合はエラー raise serializers.ValidationError(detail='"limit" is missing!') try: bottom, upper = map(int, limit.split('-')) except ValueError: # limitのフォーマットが不正な場合はエラー raise serializers.ValidationError(detail='"limit" format is invalid!') new_data['bottom_limit'] = bottom new_data['upper_limit'] = upper return super().to_internal_value(new_data) def validate_bottom_limit(self, value): print('② --- validate_bottom_limit(): 下限値のフィールド検証') return value def validate_upper_limit(self, value): print('② --- validate_upper_limit(): 上限値のフィールド検証') return value def validate(self, data): print('③ --- validate(): 下限値・上限値の複合検証') return data |
1 2 3 4 5 6 |
★ --- to_internal_value(): 検証前の調整 ① --- TestValidator: バリデータでの検証 ② --- validate_upper_limit(): 上限値のフィールド検証 ① --- TestValidator: バリデータでの検証 ② --- validate_bottom_limit(): 下限値のフィールド検証 ③ --- validate(): 下限値・上限値の複合検証 |
to_internal_value()の注意点
一件便利そうな関数ですが、注意点が1つ。to_internal_value()
はシリアライザクラスのベースクラスであるSerializer
で定義されているメソッドです。そのため、子クラスであるTestSerializer
でto_internal_value()
を定義すると、ベースクラスのメソッドがオーバーライドされてしまいます。従って、子クラス内でsuper().to_internal_value()
を呼び出さないと、想定外の挙動を引き起こす可能性があります。
試しに、親クラスの該当メソッドを呼び出さない実装にしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def to_internal_value(self, data): print('★ --- to_internal_value(): 検証前の調整') new_data = data.copy() limit = new_data.pop('limit') if not limit: # limitが空の場合はエラー raise serializers.ValidationError(detail='"limit" is missing!') try: bottom, upper = map(int, limit.split('-')) except ValueError: # limitのフォーマットが不正な場合はエラー raise serializers.ValidationError(detail='"limit" format is invalid!') new_data['bottom_limit'] = bottom new_data['upper_limit'] = upperw # return super().to_internal_value(new_data) return new_data # -> 親クラスのメソッドは呼び出さず、単純に整形後のdictだけを返してみる |
1 2 |
★ --- to_internal_value(): 検証前の調整 ③ --- validate(): 下限値・上限値の複合検証 |
Serializer
の該当メソッドを見てみると、この関数内で①バリデータでの検証と②フィールドでの検証を実行していることが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
def to_internal_value(self, data): """ Dict of native values <- Dict of primitive datatypes. """ if not isinstance(data, Mapping): message = self.error_messages['invalid'].format( datatype=type(data).__name__ ) raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] }, code='invalid') ret = {} errors = {} fields = self._writable_fields for field in fields: validate_method = getattr(self, 'validate_' + field.field_name, None) primitive_value = field.get_value(data) try: validated_value = field.run_validation(primitive_value) # -> ここで①バリデータでの検証実行 if validate_method is not None: validated_value = validate_method(validated_value) # -> ここで②フィールドでの検証実行 except ValidationError as exc: errors[field.field_name] = exc.detail except DjangoValidationError as exc: errors[field.field_name] = get_error_detail(exc) except SkipField: pass else: self.set_value(ret, field.source_attrs, validated_value) if errors: raise ValidationError(errors) return ret |
to_internal_value()
を実装する場合は、その中で明示的にsuper().to_internal_value()
を呼び出さないと、本来実行されるはずのバリデーションが行われず、意図せず不正データがデータベースに保存される可能性があります。
まとめ
このあたりの仕様をあまり詳しく調べずに実装を進めていたせいで、ハマった経験があったので今回共有させていただきました。フレームワークやライブラリは便利な反面、その仕様を理解しておかないと想定外の動きを見せるので、公式ドキュメントやソースコードを見ることの大切さを改めて痛感しました。エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!