Формати даних: JSON, XML, TOML та бінарні формати
Формати даних: JSON, XML, TOML та бінарні формати
1. Проблема: Як передати об'єкт по мережі?
Уявімо, що наш сервіс замовлення кави обробляє запит і формує об'єкт у пам'яті:
var order = new
{
Id = 42,
Recipe = "lungo",
Volume = "300ml",
Price = new { Amount = 10.23m, Currency = "UAH" },
CreatedAt = DateTime.UtcNow
};
Цей об'єкт живе у пам'яті .NET-процесу. Але клієнт — мобільний додаток на Swift або Kotlin — працює в абсолютно іншому середовищі. Він не може прочитати пам'ять .NET.
Потрібен спосіб перетворити об'єкт у послідовність байтів (або символів), яку можна:
- Передати по мережі (через HTTP).
- Прочитати на іншому боці, в іншій мові програмування.
- Відновити до структури даних нативної для тієї мови.
Цей процес називається серіалізація (Serialization) — перетворення об'єкта у формат для передачі — та десеріалізація (Deserialization) — зворотне відновлення.
Питання — у якому форматі серіалізувати? Від цього вибору залежить швидкість, зручність розробки та сумісність систем.
2. JSON — формат за замовчуванням
JSON (JavaScript Object Notation) — це текстовий формат серіалізації даних, який став стандартом де-факто для HTTP API. Попри назву, JSON давно не прив'язаний до JavaScript і використовується у всіх мовах програмування.
Синтаксис JSON
JSON підтримує шість типів даних:
{
"id": 42,
"recipe": "lungo",
"is_ready": false,
"volume_ml": 300,
"price": 10.23,
"ingredients": ["water", "coffee"],
"machine": {
"id": "cm-001",
"location": "Київ, вул. Хрещатик, 1"
},
"completed_at": null
}
| Тип | Приклад | Примітка |
|---|---|---|
| Рядок (string) | "lungo" | Завжди у подвійних лапках |
| Число (number) | 42, 10.23 | Без розділення на int/float |
| Булевий (boolean) | true, false | Тільки lowercase |
| Null | null | Відсутність значення |
| Масив (array) | [1, 2, 3] | Упорядкований набір |
| Об'єкт (object) | {"key": "value"} | Неупорядкований набір пар |
number. Стандарт також не обмежує довжину чисел, але на практиці більшість парсерів використовують 64-бітний double, що дає лише ~15–17 значущих десяткових цифр. Для грошових сум та інших точних дробових чисел завжди використовуйте рядки або цілі числа з множником (наприклад, передавайте суму в копійках):{
"price_wrong": 10.23,
"price_correct": "10.23",
"price_as_cents": 1023
}
Серіалізація JSON у Minimal API
ASP.NET Core Minimal API має вбудовану серіалізацію JSON за допомогою System.Text.Json:
var app = WebApplication.Create(args);
app.MapGet("/v1/orders/{id}", (int id) =>
{
// ASP.NET Core автоматично серіалізує
// анонімний об'єкт у JSON
return Results.Ok(new
{
id,
recipe = "lungo",
volume_ml = 300,
price = "10.23", // рядок для точності!
currency = "UAH",
is_ready = false
});
});
app.Run();
Відповідь сервера:
HTTP/1.1 200 OK
Content-Type: application/json
{"id":1,"recipe":"lungo","volume_ml":300,"price":"10.23","currency":"UAH","is_ready":false}
Results.Ok() а не просто return object? Метод Results.Ok() явно задає статус-код 200 OK і серіалізує об'єкт у JSON. Ви також можете повернути об'єкт напряму — Minimal API серіалізує його автоматично — але явний Results.Ok() робить код читабельнішим і дозволяє контролювати статус-коди.Переваги JSON
Чому саме JSON став стандартом для HTTP API, а не, скажем, XML чи бінарний формат?
👁️ Людиночитаємість
📐 Формальність
🌍 Універсальність
🪶 Легкість
3. XML — на випадок, якщо зустрінете
XML (eXtensible Markup Language) — це формат, який домінував у API до ери JSON. Сьогодні XML у нових API зустрічається рідко, але ви точно стикнетесь з ним у SOAP-сервісах, корпоративних інтеграціях та конфігураційних файлах.
<?xml version="1.0" encoding="UTF-8"?>
<order>
<id>42</id>
<recipe>lungo</recipe>
<isReady>false</isReady>
<volumeMl>300</volumeMl>
<price currency="UAH">10.23</price>
<ingredients>
<ingredient>water</ingredient>
<ingredient>coffee</ingredient>
</ingredients>
<machine>
<id>cm-001</id>
<location>Київ, вул. Хрещатик, 1</location>
</machine>
</order>
JSON vs XML: Порівняння
| Аспект | JSON | XML |
|---|---|---|
| Читабельність | Висока | Середня (багато тегів) |
| Розмір | Компактний | Великий (закриваючі теги) |
| Типізація | Слабка (number, string, bool, null) | Через XML Schema (потужна, але складна) |
| Коментарі | ❌ Не підтримує | ✅ <!-- коментар --> |
| Атрибути | ❌ Тільки пари ключ-значення | ✅ <price currency="UAH"> |
| Простір імен | ❌ Немає | ✅ xmlns: (для уникнення колізій) |
| Парсинг у браузері | Нативна підтримка (JSON.parse) | Потребує DOMParser |
4. TOML — конфігурації та метадані
TOML (Tom's Obvious Minimal Language) — це формат, спроєктований спеціально для конфігураційних файлів. Він не використовується для передачі даних між клієнтом і сервером у HTTP API, але часто зустрічається в конфігурації сервісів, CI/CD та інструментів розробки.
# Це конфігурація API-сервера
[server]
host = "0.0.0.0"
port = 5000
environment = "production"
[database]
connection_string = "Host=localhost;Database=coffee"
max_pool_size = 20
enable_logging = false
[rate_limiting]
requests_per_minute = 100
burst_size = 20
# Масив таблиць — список ендпоінтів
[[endpoints]]
path = "/v1/orders"
methods = ["GET", "POST"]
auth_required = true
[[endpoints]]
path = "/v1/recipes"
methods = ["GET"]
auth_required = false
Чому TOML, а не JSON?
| Аспект | JSON | TOML |
|---|---|---|
| Коментарі | ❌ Не підтримує | ✅ # коментар |
| Дати | Рядок ("2024-01-15") | Нативний тип (2024-01-15T10:30:00Z) |
| Багаторядкові рядки | ❌ Потрібно \n | ✅ """...""" |
| Де використовувати | API, обмін даними | Конфігурації, метадані |
Cargo.toml(Rust)pyproject.toml(Python)- Конфігурації CI/CD (GitLab CI, тощо)
- Файли налаштувань сервісів
5. Бінарні формати: Protobuf, FlatBuffers, Avro
Текстові формати (JSON, XML) зручні для людей, але не оптимальні для машин. Коли кожний мілісекунда на рахунку — на допомогу приходять бінарні формати серіалізації.
Protocol Buffers (Protobuf)
Protocol Buffers — це бінарний формат від Google, який є основою gRPC. Дані описуються через .proto-файл (схему), після чого генерується код для серіалізації/десеріалізації.
syntax = "proto3";
message Order {
int32 id = 1;
string recipe = 2;
int32 volume_ml = 3;
string price = 4;
string currency = 5;
bool is_ready = 6;
}
Цей файл компілюється у C#-класи, які автоматично серіалізують/десеріалізують дані у компактний бінарний формат.
Порівняння форматів
| Аспект | JSON | Protobuf | FlatBuffers |
|---|---|---|---|
| Тип | Текстовий | Бінарний | Бінарний |
| Людиночитаємий | ✅ Так | ❌ Ні | ❌ Ні |
| Потребує схему | ❌ Ні | ✅ .proto-файл | ✅ .fbs-файл |
| Розмір (без стискання) | Великий | Малий | Найменший |
| Швидкість парсингу | Середня | Висока | Найвища (zero-copy) |
| Кодогенерація | Необов'язкова | Обов'язкова | Обов'язкова |
.proto-файлу. Натомість JSON-документ майже завжди можна зрозуміти, просто переглянувши його. Саме тому JSON є вибором за замовчуванням для публічних API. Бінарні формати доцільні для внутрішніх мікросервісних комунікацій, де продуктивність критична, а обидві сторони контролюються однією командою.6. Стиснення JSON: gzip, brotli, deflate
Частий аргумент проти JSON — його надлишковість: імена полів повторюються для кожного об'єкта в масиві, багато технічних символів (лапки, дужки, коми). Але цей аргумент має важливе застереження: він справедливий лише якщо ми не використовуємо стиснення.
Як працює стиснення в HTTP
Протокол HTTP має вбудовану підтримку стиснення через заголовки Accept-Encoding та Content-Encoding:
Алгоритми стиснення
gzip — найпоширеніший алгоритм стиснення для HTTP. Підтримується усіма браузерами та HTTP-клієнтами. Стискає текстовий JSON у 5–10 разів.
var builder = WebApplication.CreateBuilder(args);
// Додаємо middleware стиснення відповідей
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
var app = builder.Build();
app.UseResponseCompression();
app.MapGet("/v1/orders", () =>
{
// Відповідь буде автоматично стиснута,
// якщо клієнт надіслав Accept-Encoding: gzip
return Results.Ok(new[]
{
new { id = 1, recipe = "lungo" },
new { id = 2, recipe = "americano" },
// ... тисячі замовлень
});
});
app.Run();
Brotli — сучасний алгоритм від Google, спеціально оптимізований для текстових даних (HTML, CSS, JSON). Стискає на 15–25% краще ніж gzip, але працює повільніше при компресії.
using System.IO.Compression;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// Надаємо пріоритет Brotli над gzip
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(
options => options.Level = CompressionLevel.Optimal
);
var app = builder.Build();
app.UseResponseCompression();
app.MapGet("/v1/orders", () =>
Results.Ok(new[]
{
new { id = 1, recipe = "lungo" },
new { id = 2, recipe = "americano" }
})
);
app.Run();
Ефективність стиснення: числа
Дослідження показують, що gzip практично нівелює різницю у розмірі між JSON та бінарними форматами (Protobuf):
| Формат | Без стиснення | gzip | Brotli |
|---|---|---|---|
| JSON (100 замовлень) | ~45 KB | ~4.5 KB | ~3.8 KB |
| Protobuf (100 замовлень) | ~12 KB | ~4.0 KB | ~3.5 KB |
| Різниця | 3.7x | ~10% | ~8% |
Високопродуктивний парсинг: simdjson
Ще один аргумент проти JSON — «повільна десеріалізація». Стандартні парсери дійсно програють бінарним форматам. Але існують оптимізовані парсери, такі як simdjson, які використовують SIMD-інструкції процесора для паралельного аналізу JSON.
simdjson здатний парсити гігабайти JSON за секунду — продуктивність, якої не вистачить лише екзотичним високочастотним системам.
7. Content Negotiation: як клієнт і сервер домовляються
HTTP має вбудований механізм для узгодження формату даних — Content Negotiation. Клієнт повідомляє серверу, які формати він розуміє, через заголовок Accept:
GET /v1/orders/42 HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, br
Сервер відповідає у запрошеному форматі та вказує його у Content-Type:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Encoding: br
{"id": 42, "recipe": "lungo"}
У Minimal API це працює автоматично для JSON. Якщо вам потрібно підтримати кілька форматів:
var builder = WebApplication.CreateBuilder(args);
// Додаємо підтримку XML-серіалізації
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.MapGet("/v1/orders/{id}", (int id, HttpContext ctx) =>
{
var order = new OrderResponse(id, "lungo", 300);
// Перевіряємо, що хоче клієнт
var accept = ctx.Request.Headers.Accept
.FirstOrDefault() ?? "";
if (accept.Contains("application/xml"))
{
// Повертаємо XML
return Results.Content(
$"<order><id>{id}</id></order>",
"application/xml"
);
}
// За замовчуванням — JSON
return Results.Ok(order);
});
app.Run();
record OrderResponse(int Id, string Recipe, int VolumeMl);
8. Практичні завдання
Рівень 1: Базовий
Наступний JSON-документ містить 5 помилок. Знайдіть їх та виправте:
{
'name': "Лунго",
"price": 10.23,
"ingredients": ["water", "coffee",],
"machine_id": 001,
"is_available": True,
"description": "Довгий еспресо
з великим об'ємом води"
}
Створіть Minimal API ендпоінт GET /v1/machines/{id}, який повертає інформацію про кавову машину:
id(число)brand(рядок)model(рядок)location(об'єкт з полямиlatitudeтаlongitudeяк рядки)supported_recipes(масив рядків)is_active(булевий)price_per_cup(рядок, бо це гроші!)
Рівень 2: Порівняльний
Запишіть наступну структуру даних у форматі JSON та XML. Порахуйте кількість символів у кожному варіанті (без пробілів та переносів рядків):
Дані: Список із 3 рецептів, кожен має: id, назву, об'єм (мл), ціну, список інгредієнтів.
Дайте відповідь: на скільки відсотків XML більший за JSON?
Рівень 3: Проєктування
Для кожного з наступних сценаріїв оберіть оптимальний формат і обґрунтуйте вибір:
- Публічне API погодного сервісу для мобільних додатків
- Внутрішній мікросервіс обробки платежів (100k запитів/с)
- Конфігурація CI/CD pipeline з коментарями
- Обмін даними між IoT-датчиками та сервером (обмежений трафік)
- API для інтеграції з банківською SOAP-системою 2010 року
9. Резюме
JSON — стандарт для HTTP API
Гроші — тільки рядки
Стиснення вирівнює шанси
Формат = контекст
Далі: у наступній статті ми заглибимось у парадигми клієнт-серверних API — від RPC першого покоління до REST та сучасних технологій (gRPC, GraphQL). Розберемо, чому термін «REST API» неточний і що насправді означає «HTTP API».
Що таке API. Клієнт-серверна архітектура
Визначення програмного інтерфейсу (API), аналогії з реального світу, клієнт-серверна модель, типи API та їх роль у сучасній розробці.
Парадигми API та концепція REST
Еволюція від RPC до REST. Шість обмежень Філдінга. Чому термін «REST API» неточний. Порівняння HTTP API, gRPC та GraphQL.