Команда 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 так распространён?

Этому есть множество причин:

  • Человекочитаемый: JSON легко понимать даже тем, кто не пишет код. Часто достаточно простого console.log(), чтобы посмотреть на данные.

  • Идеально вписан в веб: Его продвигал JavaScript, а затем массово подхватили backend-фреймворки.

  • Гибкий: Можно добавить поле, убрать его или поменять тип “на лету”. Удобно… порой даже слишком.

  • Инструменты на каждом шагу: Нужно посмотреть JSON? Подойдёт любой текстовый редактор. Хотите отправить запрос? Достаточно curl.

Итог: массовое распространение и богатая экосистема.

Однако, несмотря на все эти плюсы, существует формат, который даёт мне куда большую эффективность — и для разработчиков, и для конечных пользователей.

Protobuf: слышали о нём?

С высокой вероятностью вы никогда всерьёз не работали с Protobuf. Хотя этот формат появился ещё в 2001 году внутри Google и стал публичным в 2008-м.

Он широко используется внутри Google и во множестве современных инфраструктур — особенно для общения между сервисами в микросервисных архитектурах.

Почему же он настолько незаметен в мире публичных API?

Возможно, потому что Protobuf часто ассоциируют с gRPC, и разработчики думают, что их обязательно нужно использовать вместе (что неправда). Или потому что это бинарный формат, который на первый взгляд выглядит менее “комфортным”.

Но вот почему я сам использую его почти везде.

Proto — строгая типизация и современные инструменты

С JSON вы нередко отправляете неоднозначные или никак не гарантированные данные. Встречаются, например:

  • отсутствующее поле,

  • неверный тип,

  • опечатка в ключе,

  • или просто неясно задокументированная структура.

С Protobuf такое невозможно. Всё начинается с файла .proto, где структура сообщений определена максимально точно.

Пример файла Proto3

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

  • и во многих других…

Это экономит огромное количество времени и даёт по-настоящему комфортную поддерживаемость.

Buffer — сверхэффективная бинарная сериализация

Ещё одно крупное достоинство Protobuf: это бинарный формат, изначально спроектированный быть компактным и быстрым.

Сравним его с JSON.

Пример JSON-сообщения

{
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "isActive": true
}

Размер: 78 байт (зависит от пробелов).

То же сообщение в бинарном Protobuf

→ Около 23 байт. Примерно в 3 раза компактнее — и зачастую разница ещё больше, в зависимости от структуры.

Почему? Потому что Protobuf использует:

  • компактное “varint”-кодирование для чисел

  • отсутствие текстовых ключей (их заменяют числовые теги)

  • никаких пробелов, никакого накладного текста JSON

  • оптимизированные необязательны�� поля

  • очень эффективную внутреннюю структуру

Результат:

  • меньше трафика

  • более быстрое время ответа

  • экономия мобильных данных

  • прямое влияние на пользовательский опыт

Пример: крошечный Dart-сервер на Shelf, который возвращает Protobuf

Чтобы не быть голословными, давайте сделаем минимальный HTTP-сервер на Dart с использованием пакета shelf, который вернёт наш объект User, сериализованный в Protobuf, с корректным Content-Type.

Будем считать, что сгенерированный ранее код для типа User у вас уже есть.

Создаём простой сервер на Shelf

Создайте файл 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.

Вызов Protobuf API из Dart (с помощью http)

Как только ваш сервер начинает возвращать 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 выступает в роли общего контракта, давая вам сквозную типобезопасность и стабильную, компактную сериализацию данных во всём стеке.

Однако… у JSON всё же есть одно важное преимущество

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 сообщество

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