tkherox blog

データサイエンスおよびソフトウェア開発、たまに育児についての話を書いています

DjangoのURLFieldでURLValidatorを設定できなかった話

やろうとしていたこと

事の経緯を簡単にお話すると,DjangoのRestFrameworkにて一部のモデルに対してURLFieldを用いてURL情報を保存するカラムを用意してAPIを作成していました.そんな矢先にURLに登録するデータにs3プロトコルを用いたURLを登録しようと思い,APIでPOSTリクエストを実行した時に問題は起きました.

問題との遭遇

URLFieldを設定した項目に対してS3プロトコルのURLを与えたデータをPOSTすると,なんとモデルの登録登録ができなかった...

{
    "url": [
        "Enter a valid URL."
    ]
}

Djangoのドキュメントをよく見ると,以下のようにURLFieldではvalidatorとしてURLValidatorが設定されており,デフォルトではhttp,httpsftp,ftpsのみが有効なスキームとして検証されるそう.

URL/URI scheme list to validate against. If not provided, the default list is ['http', 'https', 'ftp', 'ftps']. As a reference, the IANA website provides a full list of valid URI schemes.

では,このURLValidatorの設定オプションでschemesにs3を指定して,URLFieldのvalidatorsにカスタムしたURLValidatorを設定してあげれば解決すると考えて実施してみたのですが,s3プロトコルは一向に許可されない不正なパラメータとして認識され続けていました.

from django.db import models
from django.core.validators import URLValidator

class Test(models.Model):
    s3_validator = URLValidator(
        schemes = ('http', 'https', 'ftp', 'ftps', 's3',)
    )

    url = models.URLField(validators=[s3_validator])

docs.djangoproject.com

原因

ドキュメントを読んでも理解できなかったのでDjangoのモデル部分を定義しているURLField Classのソースコードを確認してみました.
すると以下のような記述がありdefault_validatorsがクラス変数として認識されていることが発覚.そして,上位クラスであるFieldクラスにはこのdefault_validatorsを必ず読み込む処理が記述されており,validatorsによるバリデーション指定は追加のバリデーションを設定する仕様になっていることに気づきました.

class URLField(CharField):
    widget = URLInput
    default_error_messages = {
        'invalid': _('Enter a valid URL.'),
    }
    default_validators = [validators.URLValidator()]

    def __init__(self, **kwargs):
        super().__init__(strip=True, **kwargs)

つまり,この場合はmodels.pyでURLFieldクラスのインスタンスを生成する際にvalidatorsにURLValidatorを指定したことによって2重で異なるバリデーションが設定されていたために期待する動作をしなかったということです.

解決策

具体的な解決策としては大きく2点あります.
1点目はURLFiledクラスをオーバーライドしてdefault_validatorを書き換える方法です.これによってdefault_validatorの内容が設定したバリデーションになるため期待通りの挙動をするようになります.
一方で,URLFieldを他のカラムでも利用している場合は設定したバリデーションが全てのカラムに適応されてしまうため注意が必要です.

from django.db import models
from django.forms import UrlField as DefaultUrlField
from django.core.validators import URLValidator

class UrlField(DefaultUrlField):
    default_validators = [URLValidator(schemes=('http', 'https', 'ftp', 'ftps'. 's3'))]

class Test(models.Model):
    url = models.URLField()

2点目はCharFieldにURLValidatorを設定してあげる方法です.
元々URLFieldはCharFieldを継承して定義しているため,CharFiledを用いてインスタンスを生成する際にURLValidatorを設定してあげれば挙動は同じになります. こうすることで指定したインスタンスのみに個別のバリデーションを設定することができるため,1点目のクラスをオーバーライドする方法よりも影響範囲が限定的になります.

from django.db import models
from django.core.validators import URLValidator

class Test(models.Model):
    s3_validator = URLValidator(
        schemes = ('http', 'https', 'ftp', 'ftps', 's3',)
    )

    url = models.CharField(validators=[s3_validator])

私の場合は2点目の方法を採用しました.
というのもフレームワークのクラスをオーバーライドして知らないところで影響が発生する可能性を排除したかったのと,複数のURLFieldで異なるバリデーションを利用する必要があったからです.

まとめ

今回の事象に遭遇して改めてドキュメントとソースコードをよく読むことだ大事であると認識しました.
昨今では新しいソフトウェアが目まぐるしい早さでGithub等で公開されております.ドキュメントも非常に見やすいことから内部的な処理についてあまり気にしなくても素晴らしい機能を扱うことができると思います.しかし,開発者としてはそれらを単純に利用するだけだとしても内部的な挙動はしっかり把握しておくことが重要ですね.
私も今回の学びを肝に命じて今後もソフトウェアと触れ合っていきたいと思います.