Djangoで空文字・Noneを許容する文字列Validateを作る

やりたいこと

DjangoのSerializerで、
空文字とNoneと、ついでに未入力も許容する文字列バリデータをつくる

結論

CharFieldを定義するときに、
requiredだけでなく allow_blank, allow_null も設定しよう(でも罠がある)

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_blank=True, allow_null=True)
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
True
>>> serializer2 = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
True
>>> serializer3 = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
True

やってみること

テストのためにシリアライザを用意しました。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField()

ここから ""(空文字)、None、そもそも値がない の3パターンが通るようにしていきます。

今の状態でバリデーションをかけてみる
>>> from fuga.serializers import TestSerializer
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
False
>>> serializer = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
False
>>> serializer = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
False

すべてFalseになります。

試したこと

CharFieldには状態の異なる「空」を許容するオプションが3つあります。
それらを全て許容状態にしていきます。

1. required

required はすべてのフィールドクラスに共通するバリデートで、
”引数がシリアライザに渡されたかどうか”だけを見ます。
どんな値が入っているかはチェックしません。
デフォルトはTrue。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False)
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
True

これでまず、そもそも値が存在しない場合がクリア。

2 allow_blank

allow_blank は空文字とNoneを許容するバリデートで、デフォルトはFalseです。
CharFieldでは allow_null が非推奨で、こちらが推奨されています。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(allow_blank=True)
>>> serializer = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
True

>>> serializer2 = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
True
3. allow_null

allow_null はすべてのフィールドクラスに実装されているバリデートです。
nullを許容するか否かだが、CharFieldでは空文字かnullかの判定になります。
デフォルトはFalse。
上で述べたように、CharFieldでは非推奨とされているようです。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(allow_null=True)
serializer = TestSerializer(data={'hoge': None})
serializer.is_valid()
True

serializer2 = TestSerializer(data={'hoge': ''})
serializer.is_valid()
True

注意点

allow_blankとallow_nullは上で書いたように、
一つずつオプション指定をすると、
空文字とnullの両方を許容する挙動でした。
しかししかし、required=False と組み合わせると、
allow_blankはnullを、allow_nullは空文字を許容しなくなりました。
どういうことなの……

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_null=True)
s = TestSerializer(data={'hoge': ''})
s.is_valid()
False
s2 = TestSerializer(data={'hoge': None})
s2.is_valid()
True
class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_blank=True)
s = TestSerializer(data={'hoge': ''})
s.is_valid()
True
s2 = TestSerializer(data={'hoge': None})
s2.is_valid()
False


Serializer fields - Django REST framework
では、

It is valid to set both allow_blank=True and allow_null=True, but doing so means that there will be two differing types of empty value permissible for string representations

とあるため、2つセットしたときの挙動のほうが正しそう…?な気がします。
ただ、2つ使う時はバグに気をつけろ的記述もあるため注意したいところ。

上記の挙動を踏まえて、とりあえず今回はallow_blankもallow_nullもセットしましたが、
厳密なチェックを行う場合は注意してください。

感想

required=Falseにすれば、値がないのも空文字もNullも等価な世界線から来たので、
required=Falseにしてるのにバリデートが通らなくて結構苦戦した…
Django触り始めて1か月くらい、知れば知るほどDjangoさんは奥が深い。

しかしなぜオプションの数で挙動が変わるの…?今度調査したい。します。がんばる