序列化器类验证的三种常用方式以及is_valid()函数的源码解析

先说序列化器类验证的三种常用方式

drf有自身的一个验证机制,比较简单,比如匹配字段,类型,字符串长度等,这些洗染满足不了要求,当默认的校验不能完全满足校验需求时,就需要考虑进行补充验证。

方式1:validators(了解)

针对指定序列化器字段添加 validators 选项参数补充校验。

def about_django(value):
    if 'django' not in value.lower():
        raise serializers.ValidationError("图书不是关于Django的")

    # 注意:必须返回值!!!
    return value

class BookInfoSerializer(serializers.Serializer):
    """图书数据序列化器"""
    id = serializers.IntegerField(label='ID', read_only=True)
    btitle = serializers.CharField(label='名称', max_length=20, validators=[about_django])
    bpub_date = serializers.DateField(label='发布日期')
    bread = serializers.IntegerField(label='阅读量', required=False)
    bcomment = serializers.IntegerField(label='评论量', required=False)

运行代码:

from booktest.serializers import BookInfoSerializer

if __name__ == '__main__':
    data = {'btitle': 'python', 'bpub_date': '2019-06-01'}
    serializer = BookInfoSerializer(data=data)
    res = serializer.is_valid()

    if res:
        print(serializer.validated_data)
    else:
        print(serializer.errors)

显示结果:

{'btitle': [ErrorDetail(string='图书不是关于Django的', code='invalid')]}

结果说明:

在针对 btitle 字段的内容进行校验时,除了进行基本验证,还调用了 validators 参数指定的补充验证函数 about_django,因为传递的 btitle 字段中未含有 django,所以校验失败。

方式2:validate_<field_name>

在序列化器类中定义特定方法 validate_<field_name>,针对特定字段数据进行补充验证。

class BookInfoSerializer(serializers.Serializer):
    """图书数据序列化器"""
    ...
    btitle = serializers.CharField(label='名称', max_length=20)

    def validate_btitle(self, value):
        """此方法针对btitle字段的内容进行补充验证"""
        if 'django' not in value.lower():
            raise serializers.ValidationError("图书不是关于Django的-2")

        # 注意:必须返回值!!!
        return value

运行代码:

from booktest.serializers import BookInfoSerializer

if __name__ == '__main__':
    data = {'btitle': 'python', 'bpub_date': '2019-06-01'}
    serializer = BookInfoSerializer(data=data)
    res = serializer.is_valid()

    if res:
        print(serializer.validated_data)
    else:
        print(serializer.errors)

显示结果:

{'btitle': [ErrorDetail(string='图书不是关于Django的-2', code='invalid')]}

结果说明:

在针对 btitle 字段的内容进行校验时,除了进行基本验证,还调用了 validate_btitle 函数针对 btitle 字段的内容进行补充验证,因为传递的 btitle 字段中未含有 django,所以校验失败。

方式3:validate

在序列化器类中定义 validate 方法进行补充验证,validate 可以结合多个字段的内容进行补充验证。

class BookInfoSerializer(serializers.Serializer):
    """图书数据序列化器"""
    ...
    btitle = serializers.CharField(label='名称', max_length=20)

    def validate(self, attrs):
        btitle = attrs.get('btitle')

        if 'django' not in btitle.lower():
            raise serializers.ValidationError("图书不是关于Django的-3")

        # 注意:必须返回值!!!
        return attrs

运行代码:

from booktest.serializers import BookInfoSerializer

if __name__ == '__main__':
    data = {'btitle': 'python', 'bpub_date': '2019-06-01'}
    serializer = BookInfoSerializer(data=data)
    res = serializer.is_valid()

    if res:
        print(serializer.validated_data)
    else:
        print(serializer.errors)

显示结果:

{'non_field_errors': [ErrorDetail(string='图书不是关于Django的-3', code='invalid')]}

结果说明:

在针对 btitle 字段的内容进行校验时,除了进行基本验证,还调用了 validate 函数针对 btitle 字段的内容进行补充验证,因为传递的 btitle 字段中未含有 django,所以校验失败。

上面讲述了序列化器类验证的三种常用方式,接下来围绕这三种方式展开源码解析

    def is_valid(self, raise_exception=False):
    #一般来说,会直接调用self.run_validation(self.initial_data)
    #self.initial_data就是客户端传递过来要反序列化的数据
        if not hasattr(self, '_validated_data'):
            try:
                self._validated_data = self.run_validation(self.initial_data)

进入run_validation(self.initial_data)函数看一下:

		#对readonly和data做验证的,先不管它
        (is_empty_value, data) = self.validate_empty_values(data)
        if is_empty_value:
            return data
		#假设程序跳过上步,直接执行这一步
        value = self.to_internal_value(data)#field中也有to_internal_value()方法,注意区分

进入self.to_internal_value(data)函数看一下这个函数在干嘛?

 def to_internal_value(self, data):
    	#略过部分无用代码
        ret = OrderedDict()
        errors = OrderedDict()
        #
        fields = self._writable_fields #如果验证的属性不为read_only,则返回

        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:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret#验证完数据没问题,返回
 @property
    def _writable_fields(self):
        for field in self.fields.values():#{'id': IntegerField(label='ID', read_only=True), 'username': CharField(label='用户名'), 'password': CharField(max_length=128, write_only=True), 'token': CharField(label='JWT Token', read_only=True)}
            if not field.read_only:
                yield field

这个self.fields.values()是什么鬼?干什么的?点进去看一下:

 @cached_property
    def fields(self):
        """
        A dictionary of {field_name: field_instance}.{}
        {mobile:models.CharField()}
        """
        # `fields` is evaluated lazily. We do this to ensure that we don't
        # have issues importing modules that use ModelSerializers as fields,
        # even if Django's app-loading stage has not yet run.
        #“fields”的计算是惰性的。我们这么做是为了确保我们不会
		#在导入使用ModelSerializers作为字段的模块时出现问题,
        fields = BindingDict(self)
        for key, value in self.get_fields().items():
            fields[key] = value
        return fields

其他的先不看,英文的解释已经很清楚了,就是惰性获取我们定义的ModelSerializers类的序列化属性的信息,格式是个字典。

点BindingDict(self)实例进去看一下(注意:这个self是序列化器的类的实例):

class BindingDict(MutableMapping):
    """
    This dict-like object is used to store fields on a serializer.

    This ensures that whenever fields are added to the serializer we call
    `field.bind()` so that the `field_name` and `parent` attributes
    can be set correctly.
    这个类似于dict的对象用于在序列化器中存储字段。
	这确保了无论何时将字段添加到我们调用的序列化器中
	' field.bind() '使' field_name '和' parent '属性
	可以正确设置。
    """

    def __init__(self, serializer):
        self.serializer = serializer
        self.fields = OrderedDict()

事实上,这个代码什么都没干,我们就传递了一self,下面的赋值可以说赋的都是空值,占个位,使程序不报错,后续才能用到,
再次回到 fields(self)函数:

@cached_property
    def fields(self):
        fields = BindingDict(self)#就是一个空对象实例没接下来才进行赋值
        for key, value in self.get_fields().items():
            fields[key] = value
        return fields

self.get_fields()调用了items()函数,应该是个字典,据猜测,应该是获取序列化类的所有属性和对应的field()的值,然后以字典的形式返回,先进入self.get_fields()函数看一下,印证一下猜测:

    def get_fields(self):
        """
        Returns a dictionary of {field_name: field_instance}.
        返回一个{field_name: field_instance}的字典,之前的猜测应该是对的。
        """
        # Every new serializer is created with a clone of the field instances.
        # This allows users to dynamically modify the fields on a serializer
        # instance without affecting every other serializer instance.
        #每个新的序列化器都是用字段实例的克隆创建的。
		#允许用户动态修改序列化器上的字段
		# instance而不影响其他序列化器实例。
		#是个人也看出来了,这里用的是深拷贝,连老底都copy一份,随便改

		#事实上,真正干活的是self._declared_fields,他应该是通过某种机制动态获取序列化类的信息的
		"""
		OrderedDict([('username', CharField(label='用户名')), 
		('token', CharField(label='JWT Token', read_only=True))])
		"""
		#然后把这个有序字典copy一份,返回

        return copy.deepcopy(self._declared_fields)

再次回 到fields(self)函数:

@cached_property
"""
	OrderedDict([('username', CharField(label='用户名')), 
	('token', CharField(label='JWT Token', read_only=True))])
	username是key,CharField(label='用户名')是value,很具有迷惑性
	"""
    def fields(self):
        fields = BindingDict(self)#就是一个空对象实例没接下来才进行赋值
        for key, value in self.get_fields().items():
            fields[key] = value
        return fields

就是说,通过for循环,会创建N个BindingDict()实例对象,并分对属性进行赋值,传进来的参数正是被serlializer给接收了,参照下面代码:

class BindingDict(MutableMapping):
    def __init__(self, serializer):
        self.serializer = serializer
        self.fields = OrderedDict()

然后把fields对象返回给_writable_fields()函数:

@property
    def _writable_fields(self):
        for field in self.fields.values():#{'id': IntegerField(label='ID', read_only=True), 'username': CharField(label='用户名'), 'password': CharField(max_length=128, write_only=True), 'token': CharField(label='JWT Token', read_only=True)}
			#遍历自字典,看看每个属性是否是read_only,如果不是,就返回,咱们这里是反序列化,肯定不是
            if not field.read_only:
            #注意,出现了yield关键字,说明是生成器,生出来一个返回一个
                yield field

再次回到to_internal_value()函数:

 def to_internal_value(self, data):
       #代码略。。。
        ret = OrderedDict()
        errors = OrderedDict()
        fields = self._writable_fields #如果验证的属性不为read_only,则返回
		#注意生成器的用法,fields每循环一次,就再从fields里取一个
		#事实上这个for循环就是为从序列化类中取出每个字段进行格式验证的
        for field in fields:
            validate_method = getattr(self, 'validate_' + field.field_name, None)#这个就很熟悉了,应该想通过前面写方式2我们自定义的方式去验证,如果我们确实通过方式2定义了验证函数,就会获取验证函数的地址,方便下面代码进行调用。
            primitive_value = field.get_value(data)#获取data这个key的value,这个函数考虑了getlist的情况,先不考虑这种情况
            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:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret#验证完数据没问题,返回

进入field.run_validation(primitive_value)函数看一下:

    def run_validation(self, data=empty):
        # Test for the empty string here so that it does not get validated,
        # and so that subclasses do not need to handle it explicitly
        # inside the `to_internal_value()` method.
        #如果data为空字符串类似'',或者‘  ’,则直接返回‘’,super().run_validation(data)

        if data == '' or (self.trim_whitespace and str(data).strip() == ''):
            if not self.allow_blank:
                self.fail('blank')
            return ''
        return super().run_validation(data)

接下调用super().run_validation(data)函数,进去看一下:

    def run_validation(self, data=empty):
    	#判断是不是readonly 是不是Empty,是不是None,很显然,一般来说都不是,
        (is_empty_value, data) = self.validate_empty_values(data)#(False, data) or (True, data)
        if is_empty_value:
            return data
        value = self.to_internal_value(data)#
        self.run_validators(value)
        return value

接下里进入self.to_internal_value(data)看一下:

    def to_internal_value(self, data):
        # We're lenient with allowing basic numerics to be coerced into strings,
        # but other types should fail. Eg. unclear if booleans should represent as `true` or `True`,
        # and composites such as lists are likely user error.
        #允许将数字转成字符串,但是如果data是bool类型,或者data不属于str,int,float类型,则抛出异常
        if isinstance(data, bool) or not isinstance(data, (str, int, float,)):
            self.fail('invalid')
        value = str(data)#转成字符串
        return value.strip() if self.trim_whitespace else value#再次进行空白空字符判断,无奈

然后程序执行self.run_validators(value)

然后程序再返回,接着执行self.run_validators(value)函数(注意,这里执行的不是我们自定义的那个validators()方法,别忘了之前的super(),按照c3算法,它应该是从父类进行寻找,何况我们也没通过第一种方式去定义),它主要是去验证长度符不符合要求,字段映射对不对,类型对不对,验证字符串是否包含空字符,看代码:

    def run_validators(self, value):
        """
        Test the given value against all the validators on the field,
        and either raise a `ValidationError` or simply return.
        """
        errors = []
        for validator in self.validators:
            if hasattr(validator, 'set_context'):
                warnings.warn(
                    "Method `set_context` on validators is deprecated and will "
                    "no longer be called starting with 3.13. Instead set "
                    "`requires_context = True` on the class, and accept the "
                    "context as an additional argument.",
                    RemovedInDRF313Warning, stacklevel=2
                )
                validator.set_context(self)

            try:
                if getattr(validator, 'requires_context', False):
                    validator(value, self)
                else:
                    validator(value)
            except ValidationError as exc:
                # If the validation error contains a mapping of fields to
                # errors then simply raise it immediately rather than
                # attempting to accumulate a list of errors.
                if isinstance(exc.detail, dict):
                    raise
                errors.extend(exc.detail)
            except DjangoValidationError as exc:
                errors.extend(get_error_detail(exc))
        if errors:
            raise ValidationError(errors)

上述代码执行完,再次回到另一个to_internal_value()方法

    def to_internal_value(self, data):
        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:
                set_value(ret, field.source_attrs, validated_value)

        if errors:
            raise ValidationError(errors)

        return ret#验证完数据没问题,返回

代码应该是执行到了这一句,validated_value = field.run_validation(primitive_value),返回验证后的字段的值
接下来要执行validate_method(validated_value)函数,这个应该是我们自定义的,没定义的化,就跳过。
我没看源代码试过定义默认的验证方法,即使有的话,也只是走个过程,不处理,就直接把值返回了。

set_value(ret, field.source_attrs, validated_value)
大概就是把序列化属性和值放到字典里,事实上,它还考虑了其他情况,接受嵌套键的列表,这个我不是很懂,据猜测,应该是连接模型类进行验证,这个保留个人意见。

set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}。

函数返回到最开始写的那个run_validation(self, data=empty)函数

那这个for循环第一圈就结束了,接下来验证其他字段和值,过程一样。直到所有字段验证完。

    def run_validation(self, data=empty):
        """
        We override the default `run_validation`, because the validation
        performed by validators and the `.validate()` method should
        be coerced into an error dictionary with a 'non_fields_error' key.
        """
        (is_empty_value, data) = self.validate_empty_values(data)
        if is_empty_value:
            return data

        value = self.to_internal_value(data)#field中也有to_internal_value()方法,注意区分
        try:
            self.run_validators(value)#调用我们自己的方法,这是第一种种验证方式
            value = self.validate(value)#掉我们自己的方法,这是第三种验证方式
            assert value is not None, '.validate() should return the validated data'
        except (ValidationError, DjangoValidationError) as exc:
            raise ValidationError(detail=as_serializer_error(exc))

        return value

接下来执行try里的代码self.run_validators(value)#这是调用第三种验证方式,我们没写,直接跳过。 接下来执行self.validate(value)函数,这是我们自定义的第三种方式,这个我们在序列化类中是定义的了,要执行。

    def validate(self, attrs):
        # 获取 username 和 password
        username = attrs['username']
        password = attrs['password']

        # 进行用户名和密码校验
        try:
            # is_staff=True:表示要查找的是管理员用户
            user = User.objects.get(username=username, is_staff=True)
        except User.DoesNotExist:
            raise serializers.ValidationError('用户名或密码错误')
        else:
            # 校验密码
            if not user.check_password(password):
                raise serializers.ValidationError('用户名或密码错误')

            # 给 attrs 中添加 user 属性,保存登录用户
            attrs['user'] = user

        return attrs

再次回到is_valid()方法:

    def is_valid(self, raise_exception=False):
        assert hasattr(self, 'initial_data'), (
            'Cannot call `.is_valid()` as no `data=` keyword argument was '
            'passed when instantiating the serializer instance.'
        )

        if not hasattr(self, '_validated_data'):
            try:
                self._validated_data = self.run_validation(self.initial_data)
            except ValidationError as exc:
                self._validated_data = {}
                self._errors = exc.detail
            else:
                self._errors = {}

        if self._errors and raise_exception:
            raise ValidationError(self.errors)

        return not bool(self._errors)

self._validated_data = self.run_validation(self.initial_data)这句代码已经执行完了,并且获得了从我们自定义的第三种验证方式验证后的数据了,此时证明验证通过。所以,self._errors = {},然后return not bool(self._errors),这个返回的肯定是True了。
自此,isvalid()函数执行完,程序进入save()
序列化器类验证的三种常用方式以及is_valid()函数的源码解析

上一篇:Vue中:error 'XXXXX' is not defined no-undef解决办法


下一篇:【解决问题】PDF express检查论文格式报错:Errors: Font Helvetica is not embedded 解决办法