marshmallowでschemaに制限を加えてみる

marshmallowでschemaに制限を加えてみる。この制限をprotocolと呼ぶとしてこのprotocolの実装をどうするかと言うとこんな感じ。 marshmallowの流儀に従うならSchemaOptsを拡張するのが自然そう。

marshmallow_protocol.py

class CustomOpts(SchemaOpts):
    protocol = None


class CustomMeta(SchemaMeta):
    def __init__(self, name, bases, attrs):
        super().__init__(name, bases, attrs)
        k = "_protocol_verified"
        protocol = self.Meta.protocol
        if protocol is not None and k not in self.__dict__:
            protocol(self)
            setattr(self, k, protocol)


class ProtocolError(Exception):
    pass


class Schema(BaseSchema, metaclass=CustomMeta):
    class Meta:
        protocol = None

こんな感じ。marshmallowは何らかの設定(Option)を取る時にMetaというクラスに書くことが多いのだけれど。 (このような実装になっているパッケージは結構多く存在している。一番身近な例でいうとdjangoかもしれない)

このmetaにprotocolという属性を追加する。

protocol

protocolは、schemaを引数として呼べるcallable。やばかったらProtocolErrorの例外が発生する。 ついでに、何度も呼ばれることが無いようcacheする意味合いとどのprotocolで検証されたかを把握するために、_protocol_verifiedというフィールドに利用されたprotocolを埋め込む。

呼ばれるタイミングは、クラス定義時。

利用例

metadataに制限を加えてみようというprotocol

例えば、marshmallowのfields.Fieldはmetadataを付加できる。このmetadataに制限を加えてみようというprotocol。

もう少し問題認識を共有しておくと、まず、marshmallow.fields.Fieldの__init__()は以下の様になっている。

class Field(FieldABC):
    def __init__(self, default=missing_, attribute=None, load_from=None, dump_to=None,
                 error=None, validate=None, required=False, allow_none=None, load_only=False,
                 dump_only=False, missing=missing_, error_messages=None, **metadata):

        # do something()

        self.metadata = metadata

metadataは自由につけられるので例えば以下の様なSchemaを定義してしまえる。

from marshmallow import Schema, fields, validate


class S(Schema):
    v = fields.Integer(validator=validate.Range(max=10))

ちなみに、これはvalidateをvalidatorとtypoしている。このschemaはmetadataとしてvalidate.Rangeのオブジェクトが格納されたvalidな定義。 もちろん、設定したRangeによるvalidateはload時には動かないわけだけれど。

S().load({"v": 1000000000000000})  # エラーは発生しない

このとき、metadataに制限を加えてあげてみることにする。例えば、swaggerなどのようにmetadata的なものをx_ではじまるものだけに絞るというprotocolを定義する。(swaggerの場合はx-ではじまるもの)

from marshmallow.schema import BaseSchema
from marshmallow_protocol import CustomMeta, ProtocolError


def x_vendor_prefix_only_metadata(cls):
    for f in cls._declared_fields.values():
        for k in f.metadata.keys():
            if not k.startswith("x_"):
                raise ProtocolError(k)


class Schema(BaseSchema, metaclass=CustomMeta):
    class Meta:
        protocol = x_vendor_prefix_only_metadata

定義したprotocolがdefaultのSchemaを作り、これを利用してみる。

from marshmallow import fields, validate


class S(Schema):
    # marshmallow_protocol.ProtocolError: validator
    v = fields.Integer(validator=validate.Range(max=10))

validatorはx_で始まっていないのでエラーになる(エラーメッセージが少し雑すぎるかもしれない)。

もちろん、typoしていなければエラーにはならないし。protocolを無効にしてあげればエラーにならない。

from marshmallow import fields, validate



class S2(Schema):
    v = fields.Integer(validate=validate.Range(max=10), x_ja="値")


class Ignored(Schema):
    class Meta:
        protocol = None

    v = fields.Integer(validator=validate.Range(max=10))

おしまい。