Django REST framework API 指南(12):驗證器

wcode發表於2018-03-15

官方原文連結
本系列文章 github 地址
轉載請註明出處

驗證器

大多數情況下,您在 REST framework 中處理驗證時,只需依賴預設的欄位驗證,或者在序列化類或欄位類上編寫明確的驗證方法。

但是,有時你會希望將驗證邏輯放置到可重用元件中,以便在整個程式碼庫中輕鬆地重用它。這可以通過使用驗證器函式和驗證器類來實現。

REST framework 中的驗證

Django REST framework 序列化器中的驗證與 Django ModelForm 類中驗證的工作方式有點不同。

使用 ModelForm,驗證一部分在表單上執行,一部分在模型例項上執行。使用 REST framework ,驗證完全在序列化類上執行。這是有優勢的,原因如下:

  • 它使問題適當的分離,讓程式碼行為變的更加清晰。
  • 使用快捷的 ModelSerializer 類和使用顯式的 Serializer 類可以輕鬆切換。任何用於 ModelSerializer 的驗證行為都很容易複製。
  • 列印序列化類例項的 repr 將會顯示它應用的驗證規則。在模型例項上沒有額外的隱藏驗證行為(因為全在序列化類上)。

當你使用 ModelSerializer 時,所有驗證都會自動為你處理。如果你想要改為使用 Serializer 類,那麼你需要明確定義驗證規則。

舉個例子

作為 REST framework 如何使用顯式驗證的示例,我們將採用一個簡單的模型類,該類具有唯一性約束的欄位。

class CustomerReportRecord(models.Model):
    time_raised = models.DateTimeField(default=timezone.now, editable=False)
    reference = models.CharField(unique=True, max_length=20)
    description = models.TextField()
複製程式碼

下面是一個基本的 ModelSerializer ,我們可以使用它來建立或更新 CustomerReportRecord 的例項:

class CustomerReportSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomerReportRecord
複製程式碼

現在使用 manage.py shell 開啟 Django shell

>>> from project.example.serializers import CustomerReportSerializer
>>> serializer = CustomerReportSerializer()
>>> print(repr(serializer))
CustomerReportSerializer():
    id = IntegerField(label='ID', read_only=True)
    time_raised = DateTimeField(read_only=True)
    reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
    description = CharField(style={'type': 'textarea'})
複製程式碼

這裡有趣的是 reference 欄位。我們可以看到唯一性約束由序列化欄位上的驗證器明確執行。

由於這種更明確的風格,REST framework 包含一些在核心 Django 中沒有的驗證器類。這些類在下面詳細說明。


UniqueValidator

該驗證器可用於在模型欄位上強制實施 unique=True 約束。它需要一個必需的引數和一個可選的 messages 引數:

  • queryset 必須 - 這是驗證唯一性的查詢集。
  • message - 驗證失敗時使用的錯誤訊息。
  • lookup - 用於查詢已驗證值的現有例項。預設為 'exact'

這個驗證器應該應用於序列化欄位,如下所示:

from rest_framework.validators import UniqueValidator

slug = SlugField(
    max_length=100,
    validators=[UniqueValidator(queryset=BlogPost.objects.all())]
)
複製程式碼

UniqueTogetherValidator

此驗證器可用於在模型例項上強制實施 unique_together 約束。它有兩個必需的引數和一個可選的 messages 引數:

  • queryset 必須 - 這是驗證唯一性的查詢集。
  • fields 必須 - 一個存放欄位名稱的列表或者元組,這個集合必須是唯一的(意思是集合中的欄位代表的一組值不能同時出現在兩條資料中)。這些欄位必須都是序列化類中的欄位。
  • message - 驗證失敗時使用的錯誤訊息。

驗證器應該應用於序列化類,如下所示:

from rest_framework.validators import UniqueTogetherValidator

class ExampleSerializer(serializers.Serializer):
    # ...
    class Meta:
        # ToDo items belong to a parent list, and have an ordering defined
        # by the 'position' field. No two items in a given list may share
        # the same position.
        validators = [
            UniqueTogetherValidator(
                queryset=ToDoItem.objects.all(),
                fields=('list', 'position')
            )
        ]
複製程式碼

注意: UniqueTogetherValidation 類總是施加一個隱式約束,即它所應用的所有欄位都是按需處理的。具有 default 值的欄位是一個例外,因為它們總是提供一個值,即使在使用者輸入中省略了這個值。


UniqueForDateValidator

UniqueForMonthValidator

UniqueForYearValidator

這些驗證器可用於強制實施模型例項上的 unique_for_dateunique_for_monthunique_for_year 約束。他們有以下引數:

  • queryset 必須 - 這是驗證唯一性的查詢集。
  • field 必須 - 在給定日期範圍內需要被驗證唯一性的欄位的名稱。該欄位必須是序列化類中的欄位。
  • date_field 必須 - 將用於確定唯一性約束的日期範圍的欄位名稱。該欄位必須是序列化類中的欄位。
  • message - 驗證失敗時使用的錯誤訊息。

驗證器應該應用於序列化類,如下所示:

from rest_framework.validators import UniqueForYearValidator

class ExampleSerializer(serializers.Serializer):
    # ...
    class Meta:
        # Blog posts should have a slug that is unique for the current year.
        validators = [
            UniqueForYearValidator(
                queryset=BlogPostItem.objects.all(),
                field='slug',
                date_field='published'
            )
        ]
複製程式碼

我解釋下,上面例子的意思是,在 published 日期所在的年份中,slug 欄位的值必須唯一,注意,不是要和 published 完全相等的日期,而是年份相等。unique_for_dateunique_for_month 同理。

用於驗證的日期欄位應該始終存在於序列化類中。你不能簡單地依賴模型類 default=...,因為預設值在驗證執行之後才會生成。

你可能需要使用幾種樣式,具體取決於你希望 API 如何展現。如果你使用的是 ModelSerializer ,可能只需依賴 REST framework 為你生成的預設值,但如果你使用的是 Serializer 或需要更明確的控制,請使用下面演示的樣式。

與可寫日期欄位一起使用。

如果你希望日期欄位是可寫的,唯一值得注意的是你應該確保它始終可用於輸入資料中,可以通過設定 default 引數或通過設定 required=True 來實現。

published = serializers.DateTimeField(required=True)
複製程式碼

與只讀日期欄位一起使用。

如果你希望日期欄位可見,但使用者無法編輯,請設定 read_only=True 並另外設定 default=... 引數。

published = serializers.DateTimeField(read_only=True, default=timezone.now)
複製程式碼

該欄位對使用者不可寫,但預設值仍將傳遞給 validated_data

與隱藏的日期欄位一起使用。

如果你希望日期欄位對使用者完全隱藏,請使用 HiddenField。該欄位型別不接受使用者輸入,而是始終將其預設值返回到序列化類中的 validated_data

published = serializers.HiddenField(default=timezone.now)
複製程式碼

注意: UniqueFor<Range>Validation 類總是施加一個隱式約束,即它所應用的所有欄位都是按需處理的。具有 default 值的欄位是一個例外,因為它們總是提供一個值,即使在使用者輸入中省略了這個值。


Advanced field defaults

在序列化類的多個欄位中應用的驗證器有時不需要由 API 客戶端提供的欄位輸入,但它可以用作驗證器的輸入。

有兩種模式可能需要這種驗證:

  • 使用 HiddenField 。該欄位將出現在 validated_data 中,但不會用在序列化輸出表示中。
  • 使用 read_only=True 的標準欄位,同時也包含 default=... 引數。該欄位將用於序列化輸出表示中,但不能由使用者直接設定。

REST framework 包含一些在這種情況下可能有用的預設值。

CurrentUserDefault

可用於表示當前使用者的預設類。為了使用它,在例項化序列化類時,'request' 必須作為上下文字典的一部分提供。

owner = serializers.HiddenField(
    default=serializers.CurrentUserDefault()
)
複製程式碼

CreateOnlyDefault

可用於在 create 操作期間僅設定預設引數的預設類。在 update 期間,該欄位被省略。

它接受一個引數,這是在 create 操作期間應該使用的預設值或可呼叫引數。

created_at = serializers.DateTimeField(
    read_only=True,
    default=serializers.CreateOnlyDefault(timezone.now)
)
複製程式碼

驗證器的限制

有一些不明確的情況,你需要顯示處理驗證,而不是依賴 ModelSerializer 生成的預設序列化類。

在這些情況下,你可能希望通過為序列化類 Meta.validators 屬性指定一個空列表來禁用自動生成的驗證器。

可選欄位

預設情況下 "unique together" 驗證強制所有欄位都是 required=True。在某些情況下,你可能希望顯式將 required=False 應用於其中一個欄位,在這種情況下,驗證所需的行為是不明確的。

在這種情況下,你通常需要從序列化類中排除驗證器,並且在 .validate() 方法中或在檢視中顯式地編寫驗證邏輯。

例如:

class BillingRecordSerializer(serializers.ModelSerializer):
    def validate(self, data):
        # Apply custom validation either here, or in the view.

    class Meta:
        fields = ('client', 'date', 'amount')
        extra_kwargs = {'client': {'required': 'False'}}
        validators = []  # Remove a default "unique together" constraint.
複製程式碼

更新巢狀序列化類

將更新應用於現有例項時,唯一性驗證器將從唯一性檢查中排除當前例項。當前例項在唯一性檢查的上下文中可用,因為它作為序列化程式中的一個屬性存在,最初在例項化序列化類時已使用 instance=... 傳遞。

在巢狀序列化類上進行更新操作時,無法應用此排除,因為該例項不可用。

你可能又一次需要明確地從序列化類中移除驗證器,並將驗證約束的程式碼顯式寫入 .validate() 方法或檢視中。

除錯複雜的案例

如果你不確定 ModelSerializer 類的預設行為,那麼執行 manage.py shell 並列印序列化類例項通常是一個好主意,以便你可以檢查它自動生成的欄位和驗證器。

>>> serializer = MyComplexModelSerializer()
>>> print(serializer)
class MyComplexModelSerializer:
    my_fields = ...
複製程式碼

還要記住,在複雜情況下,明確定義序列化類通常會更好,而不是依賴預設的 ModelSerializer 行為。雖然這樣會寫更多的程式碼,但確保了最終的行為更加透明。


編寫自定義驗證器

你可以使用 Django 現有的驗證器,也可以編寫自定義驗證器。

基於函式

驗證器可以是任何可呼叫物件,在失敗時引發 serializers.ValidationError

def even_number(value):
    if value % 2 != 0:
        raise serializers.ValidationError('This field must be an even number.')
複製程式碼

欄位級驗證

你可以通過向 Serializer 子類新增 .validate_<field_name>方法來指定自定義欄位級驗證。

基於類

要編寫一個基於類的驗證器,請使用 __call__ 方法。基於類的驗證器很有用,因為它們允許引數化和重用行為。

class MultipleOf(object):
    def __init__(self, base):
        self.base = base

    def __call__(self, value):
        if value % self.base != 0:
            message = 'This field must be a multiple of %d.' % self.base
            raise serializers.ValidationError(message)
複製程式碼

使用 set_context()

在一些高階的情況下,你可能想要在驗證器中獲取正在被驗證的序列化欄位。這時,你可以通過在基於類的驗證器上宣告 set_context 方法來完成此操作。

def set_context(self, serializer_field):
    # Determine if this is an update or a create operation.
    # In `__call__` we can then use that information to modify the validation behavior.
    self.is_update = serializer_field.parent.instance is not None
複製程式碼

相關文章