Лучше, чем JSON: почему я перешёл на Protobuf
- суббота, 6 декабря 2025 г. в 00:00:08
Команда Go for Devs подготовила перевод статьи о том, почему автор почти десять лет не использует JSON в своих API и предпочитает Protobuf. Он объясняет, как строгая типизация, компактная бинарная сериализация и генерация кода дают разработчикам больше надёжности и скорости.

Если вы разрабатываете или используете API, с вероятностью 99% оно обменивается данными в формате JSON. Он стал фактическим стандартом современного веба. И всё же вот уже почти десять лет, создавая серверы — будь то для личных или рабочих проектов — я не использую JSON.
И меня до сих пор удивляет, что JSON настолько повсюду, хотя существуют куда более эффективные альтернативы, местами гораздо лучше подходящие для действительно современного подхода к разработке. Среди них — Protocol Buffers, или Protobuf.
В этой статье я хочу объяснить почему.
Прежде чем двигаться дальше, вернём тему в контекст.
API (Application Programming Interface) — это набор правил, которые позволяют двум системам взаимодействовать. В веб-мире REST API — те, что используют протокол HTTP и его методы (GET, POST, PUT, DELETE…) — до сих пор самые распространённые.
Когда клиент отправляет запрос серверу, он передаёт сообщение, которое содержит:
заголовки, включая хорошо знакомый Content-Type, указывающий формат сообщения (JSON, XML, Protobuf и т. д.);
тело (payload), в котором находятся сами данные;
статус ответа.
Сериализация — это процесс преобразования структуры данных в последовательность байтов, пригодную для передачи. JSON, например, сериализует данные в человекочитаемый текст.
Этому есть множество причин:
Человекочитаемый: JSON легко понимать даже тем, кто не пишет код. Часто достаточно простого console.log(), чтобы посмотреть на данные.
Идеально вписан в веб: Его продвигал JavaScript, а затем массово подхватили backend-фреймворки.
Гибкий: Можно добавить поле, убрать его или поменять тип “на лету”. Удобно… порой даже слишком.
Инструменты на каждом шагу: Нужно посмотреть JSON? Подойдёт любой текстовый редактор. Хотите отправить запрос? Достаточно curl.
Итог: массовое распространение и богатая экосистема.
Однако, несмотря на все эти плюсы, существует формат, который даёт мне куда большую эффективность — и для разработчиков, и для конечных пользователей.
С высокой вероятностью вы никогда всерьёз не работали с Protobuf. Хотя этот формат появился ещё в 2001 году внутри Google и стал публичным в 2008-м.
Он широко используется внутри Google и во множестве современных инфраструктур — особенно для общения между сервисами в микросервисных архитектурах.
Почему же он настолько незаметен в мире публичных API?
Возможно, потому что Protobuf часто ассоциируют с gRPC, и разработчики думают, что их обязательно нужно использовать вместе (что неправда). Или потому что это бинарный формат, который на первый взгляд выглядит менее “комфортным”.
Но вот почему я сам использую его почти везде.
С JSON вы нередко отправляете неоднозначные или никак не гарантированные данные. Встречаются, например:
отсутствующее поле,
неверный тип,
опечатка в ключе,
или просто неясно задокументированная структура.
С Protobuf такое невозможно. Всё начинается с файла .proto, где структура сообщений определена максимально точно.
syntax = "proto3";
message User {
int32 id = 1;
string name = 2;
string email = 3;
bool isActive = 4;
}У каждого поля есть:
строгий тип (string, int32, bool…),
числовой идентификатор (1, 2, 3…),
стабильное имя (name, email…).
Этот файл затем используется для автоматической генерации кода на выбранном языке.
Вы используете protoc:
protoc --dart_out=lib user.protoи автоматически получаете в своём Dart-коде примерно следующее:
final user = User()
..id = 42
..name = "Alice"
..email = "alice@example.com"
..isActive = true;
final bytes = user.writeToBuffer(); // Binary serialization
final sameUser = User.fromBuffer(bytes); // DeserializationНикакой ручной валидации. Никакого парсинга JSON. Никакого риска ошибиться с типами.
И этот механизм работает в:
Dart
TypeScript
Kotlin
Swift
C#
Go
Rust
и во многих других…
Это экономит огромное количество времени и даёт по-настоящему комфортную поддерживаемость.
Ещё одно крупное достоинство Protobuf: это бинарный формат, изначально спроектированный быть компактным и быстрым.
Сравним его с JSON.
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"isActive": true
}Размер: 78 байт (зависит от пробелов).
→ Около 23 байт. Примерно в 3 раза компактнее — и зачастую разница ещё больше, в зависимости от структуры.
Почему? Потому что Protobuf использует:
компактное “varint”-кодирование для чисел
отсутствие текстовых ключей (их заменяют числовые теги)
никаких пробелов, никакого накладного текста JSON
оптимизированные необязательные поля
очень эффективную внутреннюю структуру
Результат:
меньше трафика
более быстрое время ответа
экономия мобильных данных
прямое влияние на пользовательский опыт
Чтобы не быть голословными, давайте сделаем минимальный HTTP-сервер на Dart с использованием пакета shelf, который вернёт наш объект User, сериализованный в Protobuf, с корректным Content-Type.
Будем считать, что сгенерированный ранее код для типа User у вас уже есть.
Создайте файл bin/server.dart:
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';
import 'package:your_package_name/user.pb.dart'; // Adjust the path to your generated file
void main(List<String> args) async {
final router = Router()
..get('/user', _getUserHandler);
final handler = const Pipeline()
.addMiddleware(logRequests())
.addHandler(router);
final server = await shelf_io.serve(handler, InternetAddress.anyIPv4, 8080);
print('Server listening on http://${server.address.host}:${server.port}');
}
Response _getUserHandler(Request request) {
final user = User()
..id = 42
..name = 'Alice'
..email = 'alice@example.com'
..isActive = true;
final bytes = user.writeToBuffer();
return Response.ok(
bytes,
headers: {
'content-type': 'application/protobuf',
},
);
}Ключевые моменты:
User() приходит из сгенерированного Protobuf-кода.
writeToBuffer() сериализует объект в бинарный Protobuf.
Заголовок Content-Type установлен в application/protobuf, чтобы клиенты понимали, что нужно декодировать Protobuf, а не JSON.
Как только ваш сервер начинает возвращать User, закодированного в Protobuf, вы можете получить и декодировать его напрямую из Dart. Вам нужны всего лишь:
пакет http,
сгенерированные Protobuf-классы (user.pb.dart).
Создайте Dart-файл (например, bin/client.dart):
import 'package:http/http.dart' as http;
import 'package:your_package_name/user.pb.dart'; // Adjust path
Future<void> main() async {
final uri = Uri.parse('http://localhost:8080/user');
final response = await http.get(
uri,
headers: {
'Accept': 'application/protobuf',
},
);
if (response.statusCode == 200) {
// Decode the Protobuf bytes
final user = User.fromBuffer(response.bodyBytes);
print('User received:');
print(' id : ${user.id}');
print(' name : ${user.name}');
print(' email : ${user.email}');
print(' isActive : ${user.isActive}');
} else {
print('Request failed: ${response.statusCode}');
}
}С такой конфигурацией и сервер, и клиент опираются на одно и то же определение Protobuf, что гарантирует идеальное совпадение структур данных без ручной валидации и без парсинга JSON. Один и тот же файл .proto генерирует строго типизированный код по обе стороны, и клиенту с сервером просто не могут “разойтись” во мнениях насчёт формы или типа данных.
И это не ограничивается Dart: тот же подход без проблем работает, если ваш сервер написан на Go, Rust, Kotlin, Swift, C#, TypeScript или любом другом языке, который поддерживает компилятор Protobuf. Protobuf выступает в роли общего контракта, давая вам сквозную типобезопасность и стабильную, компактную сериализацию данных во всём стеке.
Protobuf-сообщения, конечно, можно декодировать — но в отличие от JSON, вы не увидите человекочитаемых имён полей. Вместо них будут числовые идентификаторы и типы “проводки” (wire types). Данные остаются осмысленными, но без соответствующей схемы .proto вы можете интерпретировать их лишь структурно, а не семантически. Поля видны, но вы не знаете, что они означают.
JSON можно прочитать и понять сразу:
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"isActive": true
}А вот бинарный Protobuf-пакет невозможно осмысленно прочитать без знания схемы:
1: 42
2: "Alice"
3: "alice@example.com"
4: trueЭто не мешает работать с Protobuf, но добавляет немного сложности:
нужны специальные инструменты
схемы нужно поддерживать и версионировать
инструменты для декодирования — обязательны
Для меня эта цена более чем оправдана с учётом производительности и эффективности, которые даёт Protobuf.
Надеюсь, эта статья вдохновит вас попробовать Protobuf. Это невероятно зрелый и очень быстрый инструмент, который всё ещё слишком мало заметен в мире публичных API.
И хотя Protobuf часто связывают с gRPC, никто не заставляет использовать их вместе. Protobuf прекрасно работает сам по себе, с любым обычным HTTP API.
Если вы ищете:
больше производительности,
больше надёжности,
меньше ошибок,
и действительно приятный опыт разработки,
то я настоятельно рекомендую попробовать Protobuf в вашем следующем проекте.

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!