Первая статья из мини‑серии про валидацию на базе Protobuf. В этой части — концепция spec‑first и protoc‑gen‑validate. В следующей поговорим про protovalidate и то, почему его вообще имеет смысл рассматривать как «следующее поколение» (или же как очередная эволюция в обратную сторону?)

Также, чтобы не пропустить следующую часть, очень рекомендую подписаться на мой телеграмм канал :)

В общем, зачем я поднимаю эту тему то?

Когда говорят про Protobuf, чаще всего всплывают несколько важных бенефитов:

  • легковесный бинарный формат, экономим трафик

  • удобно: описал .proto — сгенерил код — готово gRPC‑API

И это правда. Но для меня сейчас один из самых важных бенефитов Protobuf, это spec‑first документирование, ведь фактически у нас сразу заявленная схема работы API нашего сервиса. Она отдельно проходит ревью, по ней живут несколько сервисов (а с gRPC-Gateway и WEB), но есть ньюанс, почти все контракты что я видел в работе - это лишь половина реально нужной информации, ведь: схема без ограничений на значения — это только половина контракта.

Тип string в поле email сам по себе не говорит ничего:

  • можно ли пустую строку?

  • есть ли ограничение по длине?

  • должен ли это быть вообще email?

И чаще всего на эти вопросы ответы в самом коде сервиса, так что, говорить мы “spec‑first” - довольно кривовато :)

В этой статье хочу показать, как на этот вопрос смотрю я, и почему считаю, что описание API неполное, пока не описаны ограничения значений. А ещё — как в эту картину вписывается protoc-gen-validate и почему даже простое его внедрение уже сильно упорядочивает ваши контракты

Как мы обычно валидируем без spec‑first

Возьмём типичный gRPC‑сервис на Go. В нем у нас будет просто один хедндлер для регистрациию юзера

Вот базовая схема без какой‑либо валидации. Глядя на неё, мы понимаем только типы полей, но не можем ответить на вопросы:

  • Можно ли отправить пустой email?

  • Должен ли email соответствовать какому‑то формату?

  • Обязательно ли поле name?

  • etc….

Без spec‑first валидации все эти правила живут где‑то в коде сервера.

Что обычно происходит дальше:

  1. Внутри хендлера появляются первые if:

  2. Потом добавляются проверки длины, форматы, диапазоны

  3. Со временем логика валидации начинает дублироваться: один и тот же email проверяется и в gateway, и в сервисе A, и в сервисе B — потому что «а вдруг оттуда придёт невалидный запрос».

  4. В итоге появляются общие библиотеки-валидаторы, но они живут отдельно от схемы, и синхронизировать их с .proto приходится вручную.

Какие проблемы мы получаем:

  • Размазывание правил. Чтобы понять, что реально считается валидным запросом, приходится читать код нескольких слоев кода.

  • Рассинхрон. Один сервис обновил проверку (например, разрешил пустой middle_name), другой забыли. Gateway остался со старой логикой. Где‑то что‑то начинает странно падать.

  • Плохая обозримость. Открыв .proto, мы видим только типы. Понять, какие значения вообще допустимы, практически нереально.

И вот тут лично у меня возникает ощущение, что Protobuf в таком виде не выполняет свою главную роль документации.

Он описывает форму, но не смысл :)

А теперь давайте посмотрим, как та же схемы выглядит с protoc-gen-validate , так что ставим сам пакет для работы c validate

и дальше качаем пакет вызвав easyp mod update

Подробнее про работу с пакетами в protobuf можно почитать тут.

Дальше нам нужно прописать в контракте уже сами правила валидации

Это и есть spec‑first валидация: правила живут там же, где и описание структуры данных

Теперь контракт описывает и структуру, и ограничения. Любой, кто откроет этот .proto, сразу поймёт:

  • email обязателен, не длиннее 255 символов и должен быть валидным email‑адресом

  • name тоже обязателен и не может быть длиннее 100 символов

Картина для меня теперь выглядит так:

Схема описывает и структуру, и ограничения значений. Всё это живёт рядом, в одном файле, версионируется и ревьюится вместе.

Что хочется видеть в идеальном мире:

  • Открываю .proto и сразу понимаю:

    • какое поле обязательно;

    • какие у него границы (минимум/максимум, длина строки);

    • какой формат (email, UUID, phone и т.п.).

  • Эти ограничения машиночитаемы:

    • сервер может автоматически валидировать входящие сообщения до бизнес‑логики;

    • клиент (другой бэкенд, CLI, SDK) может вызвать ту же валидацию до отправки по сети и не тратить лишний RTT.

  • Всё это — не отдельные JSON‑схемы, не отдельные .yaml где‑то в Wiki, а именно часть Protobuf‑контракта.

Идея у protoc-gen-validate (PGV) довольно прямая:

  1. В .proto рядом с полями описываются правила валидации через специальные опции.

  2. Плагин protoc-gen-validate при генерации кода добавляет к сообщениям методы вида Validate().

  3. Вы вызываете msg.Validate() там, где вам нужно проверить входящие данные.

Очень условный пример:

После генерации у вас на Go появляется что‑то вроде:

Детали реализации зависят от конкретной версии PGV, но концептуально картинка такая: контракт описан в .proto, а код вокруг только исполняет его.

Окей, а как это выглядит в реальном gRPC‑сервисе?

Вы конечно можете так делать конечно же всегда, но будем объективны, это явно логичнее переложить в интерсепторы

В Go это можно реализовать через готовый интерсептор из grpc-ecosystem:

Дальше вы просто навешиваете этот интерсептор при поднятии сервера — и получаете:

  • единое место, где проверяются все входящие сообщения;

  • гарантии, что бизнес‑логика не увидит заведомо «битые» данные;

  • единый формат ошибок валидации.

Важно: это не отменяет доменную/бизнес‑валидацию. PGV прекрасно решает именно структурные вещи: длины, диапазоны, форматы, простые зависимости полей. Всё, что требует похода в БД, общения с внешними сервисами или сложной предметной логики, по‑прежнему живёт в коде.

Если подвести промежуточный итог, protoc-gen-validate даёт довольно много плюсов почти «из коробки»:

  • поднимает валидацию на уровень схемы — правила живут в .proto;

  • создаёт единый язык описания ограничений (аннотации вместо разнородных if и struct tag’ов);

  • позволяет встроить валидацию в инфраструктуру (интерсепторы, middleware), а не размазывать по коду;

  • даёт возможность использовать те же проверки в клиентском коде.

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

  • Привязка к генерации кода под каждый язык. Для polyglot‑систем поддержка «везде и сразу» уже не такая простая задача.

  • Невозможно (или очень неудобно) описывать более сложные, динамические правила без написания собственного кода вокруг.

И это нормально. Для большого количества проектов PGV до сих пор будет «тем самым инструментом, который надо просто взять и использовать».

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

И вот тут в историю входит protovalidate. Но! protovalide фактически попытка переродить механику валидаций в protobuf со всеми особенностями бизнес проверок, но о нем уже будет в продолжении в телеграмм канале :)

Комментарии (0)