2025, Dec 17 09:01

Переиспользуемые поля Marshmallow для Flask‑Smorest: один класс вместо копипаста

Как в Marshmallow и Flask-Smorest вынести повторяющиеся поля в собственный класс: общие метаданные и Regexp-валидация, меньше копипаста и документация API.

Когда вы создаете API на Flask с использованием flask-smorest и Marshmallow, нередко приходится повторять одни и те же ограничения для входных данных на нескольких эндпоинтах. Раз за разом копировать один и тот же тип поля с одинаковыми метаданными и валидатором RegExp — шумно и чревато ошибками при последующих правках.

Повторяющиеся поля схем: как это обычно выглядит

Ниже показан типичный паттерн, когда каждая схема заново объявляет одну и ту же строку с теми же правилами валидации и теми же описательными метаданными.

from marshmallow import Schema, fields, validate

class EndpointA:
    class InPayload(Schema):
        req_code = fields.Str(
            metadata={'description': 'Request Number', 'example': 'REQUEST12345'},
            validate=validate.Regexp(
                regex=r"^REQUEST\d{3,9}$",
                error="Input string didn't match required format - REQUEST12345"
            )
        )

class EndpointB:
    class InPayload(Schema):
        req_code = fields.Str(
            metadata={'description': 'Request Number', 'example': 'REQUEST12345'},
            validate=validate.Regexp(
                regex=r"^REQUEST\d{3,9}$",
                error="Input string didn't match required format - REQUEST12345"
            )
        )

В чем реальная проблема

Каждый эндпоинт повторяет одну и ту же настройку поля: тип, метаданные и встроенный validate.Regexp с одинаковыми параметрами. Даже если вынести экземпляр валидатора в вспомогательный объект и ссылаться на него из каждой схемы, вам все равно приходится заново прописывать конфигурацию поля и метаданные. Итог — дублирование, а дублирование ведет к рассинхронизации.

Аккуратное решение: переиспользуемое поле

В Marshmallow можно упаковать тип, метаданные и валидаторы в собственный подкласс Field. Вы один раз описываете общее поведение, а затем используете это поле в нужных местах. Так сохраняется встроенный валидатор, неизменными остаются и регулярное выражение, и текст ошибки, а шаблонный код из схем исчезает.

from marshmallow import Schema, fields, validate

class ReqNumField(fields.String):
    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            metadata={'description': 'Request Number', 'example': 'REQUEST12345'},
            validate=validate.Regexp(
                regex=r"^REQUEST\d{3,9}$",
                error="Input string didn't match required format - REQUEST12345"
            ),
            **kwargs
        )

class ServiceOne:
    class InputSchema(Schema):
        req_code = ReqNumField()

class ServiceTwo:
    class InputSchema(Schema):
        req_code = ReqNumField()

Подход не меняет семантику: поле по‑прежнему строковое, паттерн тот же — ^REQUEST\d{3,9}$, текст ошибки не меняется, а описательные метаданные остаются привязаны к полю. В любом месте, где вы используете ReqNumField, вы автоматически получаете согласованную валидацию и документацию.

Почему это важно

Централизация валидатора и метаданных убирает бесконечный копипаст между схемами. Если формат когда‑нибудь поменяется, править придется только в одном месте. Поскольку поле несет свои метаданные, сгенерированная документация API остается согласованной без дополнительных усилий. И так как решение опирается на встроенный validate.Regexp из Marshmallow, сохраняется стандартное поведение, к которому вы и стремились.

Итоги

Когда нескольким эндпоинтам нужны одинаковые правила валидации, упакуйте их в собственное поле Marshmallow и переиспользуйте. Держите логику и регулярку там, где им место, избегайте дублирования в схемах, а flask-smorest сам поднимет единую документацию из одной-единственной декларации.