- PVSM.RU - https://www.pvsm.ru -
JSON:API — это строго типизированная спецификация построения API на языке JSON. Её главная цель — минимизировать количество сетевых запросов и объем передаваемых данных, предоставляя при этом стандартизированный способ взаимодействия. Последней стабильной версией спецификации JSON:API на текущий момент (февраль 2026 года) является версия 1.1.
Каждый запрос и ответ в системе, следующей стандарту JSON:API, представляет собой JSON-объект, определяющий «верхний уровень» контракта. Структура этого объекта строго регламентирована.
Контракт обязан содержать как минимум один из следующих ключей верхнего уровня:
data — «первичные данные» контракта (основной контент запроса/ответа).
errors — массив объектов ошибок, возникших при обработке.
meta — объект для передачи нестандартной метаинформации.
Ключ расширения — поле, определяемое официально примененным расширением, например, atomic:operations.
Контракт может содержать следующие ключи верхнего уровня для расширения контекста:
jsonapi — объект, описывающий версию реализации и поддерживаемые возможности сервера.
links — объект ссылок, относящихся к документу в целом (например, ссылки на страницы пагинации).
included — массив ресурсных объектов, связанных с основными данными («включенные ресурсы»).
Для обеспечения целостности данных в контракте действуют следующие ограничения:
Ключи data и errors не должны присутствовать в одном контракте одновременно.
Если в контракте отсутствует ключ data, то массив included не должен присутствовать.
Первичные данные в data могут быть представлены в виде одиночного объекта ресурса, массива объектов ресурсов или значения null.
Логическая совокупность ресурсов (список) должна быть представлена в виде массива, даже если она содержит только один элемент или пуста.
Операция успешна с объектом data:
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "https://api.example.com/books/42/relationships/author"
},
"meta": {
"time": {
"publish": 1697718264
},
"data": {
"type": "author",
"id": "9",
"attributes": {
"name": "Alexander Pushkin"
},
"relationships": {
"book": {
"data": {
"type": "books",
"id": "42"
}
}
}
},
"included": [
{
"type": "books",
"id": "42",
"attributes": {
"title": "Капитанская дочка"
}
}
]
}
Произошел сбой с массивом errors:
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "https://api.example.com/books/42/relationships/author"
},
"meta": {
"time": {
"publish": 1697718264
},
"errors": [
{
"id": "err-001",
"status": "404",
"code": "AuthorNotFound",
"title": "Resource not found",
"detail": "The author relationship for the book with ID 42 could not be found.",
"source": {
"pointer": "/data/relationships/author",
"parameter": "id"
},
"links": {
"about": "https://api.example.com/docs/errors/author-not-found"
},
"meta": {
"referenceId": "42"
}
}
]
}
Ключ data это сердце контракта JSON:API. В нем передаются «первичные данные», ради которых делался запрос.
В зависимости от того, что запрашивалось, data может принимать три вида:
Одиночный ресурс (объект): когда запрашивается конкретная запись.
{
"data": {
"type": "articles",
"id": "1"
}
}
Коллекция ресурсов (массив): когда запрашивается список или результат поиска.
{
"data": [
{
"type": "articles",
"id": "1"
},
{
"type": "articles",
"id": "2"
}
]
}
Пустое состояние (null или []):
null — если запрашивался один ресурс, но он не найден (или связь пуста).
{
"data": null
}
[] — если запрашивался список, но он пуст.
{
"data": []
}
Первый объект внутри data называется «Resource Object» и обязан иметь два поля:
id: Уникальный идентификатор (всегда строка).
Не обязателен в случае создания ресурса
type: Тип ресурса (например, "users", "products").
Опционально внутри объекта могут быть:
attributes: Объект с данными.
relationships: Ссылки на связанные объекты.
links: Ссылки, относящиеся конкретно к этому ресурсу.
meta: Мета-информация об этом конкретном ресурсе.
Пример полного объекта data:
{
"data": {
"id": "42",
"type": "books",
"attributes": {
"title": "JSON:API в деталях",
"language": "ru"
},
"relationships": {
"author": {
"data": {
"id": "9",
"type": "authors"
}
}
},
"links": {
"self": "https://api.example.com/books/42"
},
"meta": {
"lastReviewed": "2024-02-14T12:00:00Z"
}
}
}
Ключ errors (массив объектов ошибок) это обязательный ключ в тех случаях, когда запрос не может быть выполнен успешно.
Спецификация не требует заполнения всех полей, но предлагает стандартный набор для максимальной информативности:
id: Уникальный идентификатор конкретного случая ошибки.
status: HTTP-статус код в виде строки (например, "422").
code: Внутренний код ошибки приложения (например, "InvalidEmail").
title: Краткое человекочитаемое описание проблемы.
detail: Детальное пояснение конкретно для этой ошибки (например, «Email должен содержать символ @»).
source: Объект, указывающий на причину ошибки:
pointer: Ссылка на поле в JSON-запросе (например, "/data/attributes/email").
parameter: Имя некорректного параметра в URL (например, "filter[age]").
links: Ссылка на страницу с описанием этой ошибки (ключ about).
meta: Любая дополнительная информация (например, время возникновения ошибки).
Пример полного объекта errors:
{
"errors": [
{
"id": "ERR-VLD-001",
"status": "422",
"code": "InvalidEmail",
"title": "Ошибка валидации данных",
"detail": "Указанный адрес электронной почты 'user@example' должен содержать символ '@' и доменную зону.",
"source": {
"pointer": "/data/attributes/email"
},
"links": {
"about": "https://api.example.com/docs/errors/vld-001"
},
"meta": {
"timestamp": "2026-02-14T15:30:00Z"
}
},
{
"id": "ERR-QRY-402",
"status": "400",
"code": "InvalidFilterValue",
"title": "Некорректный параметр запроса",
"detail": "Параметр фильтрации 'age' должен быть целым числом в диапазоне от 18 до 99.",
"source": {
"parameter": "filter[age]"
},
"links": {
"about": "https://api.example.com/docs/errors/query-params"
},
"meta": {
"receivedValue": "notANumber"
}
}
]
}
Корневой ключ meta это «свободный конверт» в JSON:API.
Спецификация никак не ограничивает его структуру, кроме одного условия: это должен быть объект. Он нужен для данных, которые не являются «ресурсами» (не имеют id и type).
{
"meta": {
"guid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"time": {
"publish": 1697718264
},
"publisher": {
"code": "system"
}
}
}
Ключ jsonapi является опциональным параметром в корне контракта. Он используется для передачи информации о реализации спецификации сервером, что позволяет клиенту понять, какие возможности протокола доступны.
объект jsonapi может содержать следующие ключи:
version (string): Указывает наивысшую версию спецификации JSON:API, которую поддерживает сервер.
ext (array): Массив строк (URI), содержащий список всех примененных расширений (extensions). Расширения позволяют изменять базовое поведение спецификации, и они должны поддерживаться и клиентом, и сервером.
profile (array): Массив строк (URI), содержащий список примененных профилей. В отличие от расширений, профили предоставляют дополнительные соглашения, которые клиент может безопасно игнорировать, если не знает, как их обрабатывать.
meta (object): Объект метаданных, содержащий любую нестандартную информацию о реализации API.
{
"jsonapi": {
"version": "1.1",
"ext": [
"https://jsonapi.org/ext/atomic"
],
"profile": [
"https://jsonapi.org/profiles/ethanresnick/cursor-pagination"
],
"meta": {
"apiVersion": "v2.4.0",
"buildTimestamp": "2024-05-20T12:00:00Z",
"requestId": "req-99123-abc",
"serverNode": "us-east-1-node-01",
"supportEmail": "api-support@example.com"
}
}
}
Ключ links в корне документа JSON:API это набор ссылок, которые помогают клиенту «ориентироваться» в API, не вычисляя URL-адреса вручную. Это реализация концепции HATEOAS (Hypermedia as the Engine of Application State).
Основные ссылки
self: Ссылка на текущий запрос. Это URL, по которому был получен данный документ. Полезно для обновления данных (рефреша).
related: Ссылка на связанные данные (если запрос был связан с отношениями).
Ссылки пагинации (самые частые). Если данных много и они разбиты на страницы, используются следующие ключи:
first: Ссылка на первую страницу данных.
last: Ссылка на последнюю страницу данных.
prev: Ссылка на предыдущую страницу (относительно текущей).
next: Ссылка на следующую страницу.
{
"links": {
"self": "https://api.example.com/articles?page[number]=2",
"prev": "https://api.example.com/articles?page[number]=1",
"next": "https://api.example.com/articles?page[number]=3",
"first": "https://api.example.com/articles?page[number]=1",
"last": "https://api.example.com/articles?page[number]=10"
}
}
Параметр related в корне используется реже, чем self, но он критически важен, когда запрашивается не сам ресурс, а его связи.
Согласно спецификации, related указывает на данные, которые относятся к текущему запросу, но технически являются «соседними» или производными.
Самый частый случай использования related это когда обращаются к эндпоинту связи.
Запрос: GET /articles/1/relationships/author («Кто автор статьи №1?»)
Ответ:
{
"links": {
"self": "https://api.example.com/articles/1/relationships/author",
"related": "https://api.example.com/articles/1/author"
},
"data": {
"type": "people",
"id": "9"
}
}
self: Ссылка на саму «связку» (позволяет её изменить, например, методом PATCH).
related: Ссылка на полный объект этого автора. Если перейти по ней, вы получите не просто id и type, а все атрибуты автора (имя, биографию и т.д.).
Иногда related может указывать на ресурс, который послужил «источником» для текущей коллекции.
Запрос: GET /articles/1/comments
Ответ:
{
"links": {
"self": "https://api.example.com/articles/1/comments",
"related": "https://api.example.com/articles/1"
},
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
Здесь related ведет обратно к статье, которой принадлежат эти комментарии. Это позволяет клиенту легко вернуться к родительскому объекту.
Ссылка в JSON:API может быть представлена двумя способами:
Простая строка: Просто URL
"self": "https://api.example.com/articles/1"
Объект ссылки: Если нужно передать дополнительные метаданные.
"self": {
"href": "https://api.example.com/articles/1/pdf",
"meta": {
"contentType": "application/pdf",
"fileSize": "2.4 MB"
}
}
Массив included является вспомогательным ключом корневого уровня, предназначенным для формирования составных документов. Его основная задача это оптимизация сетевого взаимодействия путем передачи связанных ресурсов в рамках одного HTTP-ответа.
Согласно стандарту JSON:API, использование included регулируется следующими правилами:
В массив могут быть включены только те ресурсы, которые имеют хотя бы одну связь с первичными данными (data) или другими ресурсами внутри included. «Сиротские» (несвязанные) объекты не допускаются.
Массив included не должен присутствовать в документе, если в корне отсутствует ключ data.
Каждый объект в included является полноценным ресурсным объектом. Он обязан содержать ключи id и type, а также может включать attributes, relationships, links и meta.
Ресурс не должен дублироваться. Если на один и тот же объект ссылаются несколько ресурсов из data, в массиве included он представлен в единственном экземпляре.
{
"data": {
"type": "books",
"id": "42",
"attributes": {
"title": "Спецификация JSON:API"
},
"relationships": {
"author": {
"data": {
"type": "people",
"id": "9"
}
}
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"name": "Алексей Иванов",
"role": "Автор"
},
"links": {
"self": "https://api.example.com/people/9"
}
}
]
}
Вместо того чтобы изобретать велосипед в каждом новом контроллере, спецификация предлагает жесткий, но справедливый стандарт. Query-параметры здесь — это мощный декларативный язык запросов. Они позволяют клиенту точно определять структуру, объем и вложенность, минимизируя количество сетевых запросов и нагрузку на сервер.
Представьте экран в банковском приложении, где пользователь видит детали покупки. Нам нужно отобразить:
Сумму и валюту (из самой Транзакции).
Название и иконку магазина (из связанного Мерчанта).
Последние 4 цифры карты (из связанного Счета/Карты).
ФИО персонального менеджера, если счет премиальный (связь через уровень).
Без include вам пришлось бы сделать 4 запроса. Первой запрос мог бы GET /transactions/abc-123.
Результат:
{
"data": {
"type": "transactions",
"id": "abc-123",
"attributes": {
"amount": -1500,
"currency": "RUB"
},
"relationships": {
"merchant": {
"data": {
"type": "merchants",
"id": "m-55"
}
},
"account": {
"data": {
"type": "accounts",
"id": "acc-99"
}
}
}
}
}
С include JSON:API запрос выглядит так: GET /transactions/abc-123?include=merchant,account,account.manager
Результат:
{
"data": {
"type": "transactions",
"id": "abc-123",
"attributes": {
"amount": -1500,
"currency": "RUB"
},
"relationships": {
"merchant": {
"data": {
"type": "merchants",
"id": "m-55"
}
},
"account": {
"data": {
"type": "accounts",
"id": "acc-99"
}
}
}
},
"included": [
{
"type": "merchants",
"id": "m-55",
"attributes": {
"name": "Starbucks",
"category": "Кафе"
}
},
{
"type": "accounts",
"id": "acc-99",
"attributes": {
"mask": "**** 1234"
},
"relationships": {
"manager": {
"data": {
"type": "staff",
"id": "s-7"
}
}
}
},
{
"type": "staff",
"id": "s-7",
"attributes": {
"name": "Александр",
"phone": "+7..."
}
}
]
}
Как в примере выше (account.manager), мы можем «пробивать» связи любой глубины. Но обычно ограничивает глубину (например, не более 3 уровней), чтобы предотвратить тяжелые SQL-запросы, которые могут замедлить базу данных.
Часто include работает вместе с правами доступа. Если у клиента нет доступа к счету, то запрос include=account вернет транзакцию, но массив included будет пустым, а в relationships не будет данных.
Если include — это способ собрать «пазл» данных из разных таблиц, то fields (или Sparse Fieldsets) — это прецизионный скальпель. Этот параметр позволяет клиенту сказать серверу: «Мне не нужны все твои секреты, дай только вот эти конкретные поля».
В банковской сфере это не просто вопрос эстетики, а вопрос производительности и безопасности.
Представьте, что объект «Счет» (accounts) в базе данных банка содержит 40 полей: от даты открытия и лимитов до внутренних технических флагов и истории проверок.
Без fields: Вы загружаете огромный JSON весом в 10 Кб ради того, чтобы показать только название счета.
С fields: Вы получаете компактную посылку в 200 байт.
Синтаксис JSON:API требует указывать тип ресурса в квадратных скобках, так как в одном запросе могут участвовать разные сущности (особенно при использовании include).
Мы хотим увидеть список транзакций, но нам нужны только сумма и дата. Остальное (описание, статус, тип операции) нас сейчас не интересует.
GET /transactions?fields[transactions]=amount,created_at
Результат:
{
"data": [
{
"type": "transactions",
"id": "abc-001",
"attributes": {
"amount": -1500,
"created_at": "2023-10-27T10:15:00Z"
}
// Поля 'currency', 'status' и другие — проигнорированы
},
{
"type": "transactions",
"id": "abc-002",
"attributes": {
"amount": -500,
"created_at": "2023-10-28T11:09:00Z"
}
// Поля 'currency', 'status' и другие — проигнорированы
}
]
}
Самая мощная сторона fields раскрывается, когда вы запрашиваете связанные данные. Вы можете ограничить поля как для основного ресурса, так и для всех включенных.
Запрос: «Дай мне транзакцию (только сумму) и данные счета (только последние 4 цифры)».
GET /transactions/abc-123?include=account&fields[transactions]=amount&fields[accounts]=mask
Вы можете проектировать фронтенд так, чтобы он запрашивал только публичные данные. Даже если в модели staff (менеджер) есть поле home_address, фронтенд запросит fields[staff]=name,photo, и лишние данные физически не покинут защищенный контур сервера.
Для мобильного приложения, работающего в роуминге или в зоне слабого 4G, разница в размере JSON-ответа в 5–10 раз — это разница между «приложение летает» и «приложение тормозит».
Бэкенды анализируют параметр fields и делают SELECT только тех колонок, которые запрошены, вместо SELECT *. В масштабах банка с миллионами транзакций это колоссально экономит ресурсы памяти.
Согласно спецификации, если вы использовали fields для определенного типа, сервер вернет только указанные поля. Если вы забыли добавить id, не переживайте — id и type всегда возвращаются по умолчанию, так как это «паспорт» ресурса.
Atomic Operations (Атомарные операции) это официальное расширение JSON:API 1.1, которое позволяет клиенту группировать несколько действий (создание, обновление, удаление) в один HTTP-запрос и выполнять их как единую транзакцию. Atomic Operations решает две ключевые задачи: минимизацию количества сетевых запросов и обеспечение атомарности данных (принцип «все или ничего»). Если одна из операций в пакете завершается ошибкой, сервер обязан откатить все изменения, внесенные в рамках этого запроса.
Для активации расширения клиент и сервер должны использовать специфический Media Type в заголовках Content-Type и Accept: application/vnd.api+json;ext="https://jsonapi.org [1]"
В контрактах этого типа стандартные ключи data и included заменяются специфическими ключами расширения:
atomic:operations: Массив объектов, каждый из которых описывает одну операцию.
atomic:results: Массив результатов, возвращаемый сервером после успешного выполнения всех операций.
Каждый объект внутри atomic:operations должен содержать ключ op, определяющий тип действия:
add: Создание нового ресурса или добавление связи в коллекцию.
update: Обновление атрибутов или связей существующего ресурса.
remove: Удаление ресурса или разрыв связи.
Критически важная особенность расширения это использование lid (Local ID). Это временный идентификатор, который позволяет ссылаться на ресурсы, создаваемые внутри этого же запроса.
Пример сценария: Создание автора и его книги одновременно.
Первая операция создает автора и присваивает ему "lid": "new-author".
Вторая операция создает книгу и в поле relationships ссылается на автора через тот же "lid": "new-author".
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "authors",
"lid": "author-1",
"attributes": {
"name": "Николай Гоголь"
}
}
},
{
"op": "add",
"data": {
"type": "books",
"attributes": {
"title": "Мертвые души"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"lid": "author-1"
}
}
}
}
}
]
}
Cursor Pagination (пагинация на основе курсоров) это ��аиболее производительный метод разделения данных на страницы. В отличие от классического метода offset (пропусти N строк), курсор не использует порядковый номер записи. Он опирается на указатель (курсор) на конкретный элемент, после которого нужно продолжить чтение.
Стабильность (No Drift): Если пока пользователь смотрит первую страницу, в начало списка добавят новую запись, при offset-пагинации данные «сдвинутся», и на второй странице пользователь увидит дубликат. Курсор же жестко привязан к элементу, поэтому дубликатов или пропусков не бывает.
Высокая производительность: Базе данных не нужно сканировать и отбрасывать тысячи строк, чтобы добраться до нужной (проблема OFFSET 1000000). С помощью курсора база делает эффективный прыжок по индексу сразу к нужной записи.
Идеально для бесконечных лент: Именно этот метод используют X, Slack и Twitter для бесконечной прокрутки.
Когда применяется этот профиль, ссылки в объекте links начинают использовать специфические параметры.
{
"links": {
"self": "https://api.example.com/books?page[size]=10",
"next": "https://api.example.com/books?page[size]=10&page[after]={порядковый номер записи}"
}
}
page[after]: Запрашивает ресурсы, следующие после указанного курсора.
page[before]: Запрашивает ресурсы, находящиеся перед указанным курсором (для навигации назад).
page[size]: Ограничивает количество ресурсов на одной странице.
Автор: Namnogo_Khuzhe
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/api/446316
Ссылки в тексте:
[1] https://jsonapi.org: https://jsonapi.org
[2] Источник: https://habr.com/ru/articles/1008750/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1008750
Нажмите здесь для печати.