
Первая статья из мини‑серии про валидацию на базе Protobuf. В этой части — концепция spec‑first и protoc‑gen‑validate. В следующей поговорим про protovalidate и то, почему его вообще имеет смысл рассматривать как «следующее поколение» (или же как очередная эволюция в обратную сторону?)
Также, чтобы не пропустить следующую часть, очень рекомендую подписаться на мой телеграмм канал :)
В общем, зачем я поднимаю эту тему то?
Когда говорят про Protobuf, чаще всего всплывают несколько важных бенефитов:- легковесный бинарный формат, экономим трафик
- удобно: описал .proto — сгенерил код — готово gRPC‑API
Тип string в поле email сам по себе не говорит ничего:
- можно ли пустую строку?
- есть ли ограничение по длине?
- должен ли это быть вообще email?
В этой статье хочу показать, как на этот вопрос смотрю я, и почему считаю, что описание API неполное, пока не описаны ограничения значений. А ещё — как в эту картину вписывается protoc-gen-validate и почему даже простое его внедрение уже сильно упорядочивает ваши контракты
Как мы обычно валидируем без spec‑first
Возьмём типичный gRPC‑сервис на Go. В нем у нас будет просто один хедндлер для регистрациию юзера
Вот базовая схема без какой‑либо валидации. Глядя на неё, мы понимаем только типы полей, но не можем ответить на вопросы:
- Можно ли отправить пустой email?
- Должен ли email соответствовать какому‑то формату?
- Обязательно ли поле name?
- etc….
Что обычно происходит дальше:
- Внутри хендлера появляются первые if:

- Потом добавляются проверки длины, форматы, диапазоны
- Со временем логика валидации начинает дублироваться: один и тот же email проверяется и в gateway, и в сервисе A, и в сервисе B — потому что «а вдруг оттуда придёт невалидный запрос».
- В итоге появляются общие библиотеки-валидаторы, но они живут отдельно от схемы, и синхронизировать их с .proto приходится вручную.
- Размазывание правил. Чтобы понять, что реально считается валидным запросом, приходится читать код нескольких слоев кода.
- Рассинхрон. Один сервис обновил проверку (например, разрешил пустой middle_name), другой забыли. Gateway остался со старой логикой. Где‑то что‑то начинает странно падать.
- Плохая обозримость. Открыв .proto, мы видим только типы. Понять, какие значения вообще допустимы, практически нереально.
Он описывает форму, но не смысл :)
А теперь давайте посмотрим, как та же схемы выглядит с 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‑контракта.
- В .proto рядом с полями описываются правила валидации через специальные опции.
- Плагин protoc-gen-validate при генерации кода добавляет к сообщениям методы вида Validate().
- Вы вызываете msg.Validate() там, где вам нужно проверить входящие данные.

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

Детали реализации зависят от конкретной версии PGV, но концептуально картинка такая: контракт описан в <strong>.proto</strong>, а код вокруг только исполняет его.
Окей, а как это выглядит в реальном gRPC‑сервисе?

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

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

Дальше вы просто навешиваете этот интерсептор при поднятии сервера — и получаете:
- единое место, где проверяются все входящие сообщения;
- гарантии, что бизнес‑логика не увидит заведомо «битые» данные;
- единый формат ошибок валидации.
Если подвести промежуточный итог, protoc-gen-validate даёт довольно много плюсов почти «из коробки»:Важно: это не отменяет доменную/бизнес‑валидацию. PGV прекрасно решает именно структурные вещи: длины, диапазоны, форматы, простые зависимости полей. Всё, что требует похода в БД, общения с внешними сервисами или сложной предметной логики, по‑прежнему живёт в коде.
- поднимает валидацию на уровень схемы — правила живут в .proto;
- создаёт единый язык описания ограничений (аннотации вместо разнородных if и struct tag’ов);
- позволяет встроить валидацию в инфраструктуру (интерсепторы, middleware), а не размазывать по коду;
- даёт возможность использовать те же проверки в клиентском коде.
- Привязка к генерации кода под каждый язык. Для polyglot‑систем поддержка «везде и сразу» уже не такая простая задача.
- Невозможно (или очень неудобно) описывать более сложные, динамические правила без написания собственного кода вокруг.
Но если вы смотрите дальше — хотите более универсальный рантайм, меньше генерации кода, более гибкие правила и нормальную жизнь в polyglot‑мирах, — логично начать смотреть по сторонам.