1. Введение

JSON:API — это строго типизированная спецификация построения API на языке JSON. Её главная цель — минимизировать количество сетевых запросов и объем передаваемых данных, предоставляя при этом стандартизированный способ взаимодействия. Последней стабильной версией спецификации JSON:API на текущий момент (февраль 2026 года) является версия 1.1.

2. Корневая структура

Каждый запрос и ответ в системе, следующей стандарту JSON:API, представляет собой JSON-объект, определяющий «верхний уровень» контракта. Структура этого объекта строго регламентирована.

2.1 Обязательные ключи

Контракт обязан содержать как минимум один из следующих ключей верхнего уровня:

  • data — «первичные данные» контракта (основной контент запроса/ответа).

  • errors — массив объектов ошибок, возникших при обработке.

  • meta — объект для передачи нестандартной метаинформации.

  • Ключ расширения — поле, определяемое официально примененным расширением, например, atomic:operations.

2.2 Опциональные ключи

Контракт может содержать следующие ключи верхнего уровня для расширения контекста:

  • jsonapi — объект, описывающий версию реализации и поддерживаемые возможности сервера.

  • links — объект ссылок, относящихся к документу в целом (например, ссылки на страницы пагинации).

  • included — массив ресурсных объектов, связанных с основными данными («включенные ресурсы»).

2.3 Правила зависимости

Для обеспечения целостности данных в контракте действуют следующие ограничения:

  • Ключи data и errors не должны присутствовать в одном контракте одновременно.

  • Если в контракте отсутствует ключ data, то массив included не должен присутствовать.

  • Первичные данные в data могут быть представлены в виде одиночного объекта ресурса, массива объектов ресурсов или значения null.

  • Логическая совокупность ресурсов (список) должна быть представлена в виде массива, даже если она содержит только один элемент или пуста.

2.4 Примеры корневой структуры

Операция успешна с объектом 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"
      }
    }
  ]
}

3. Корневой ключ data

Ключ data это сердце контракта JSON:API. В нем передаются «первичные данные», ради которых делался запрос.

3.1 Виды ключа data

В зависимости от того, что запрашивалось, data может принимать три вида:

Одиночный ресурс (объект): когда запрашивается конкретная запись.

{
  "data": { 
    "type": "articles", 
    "id": "1"
  }
}

Коллекция ресурсов (массив): когда запрашивается список или результат поиска.

{
  "data": [ 
    { 
      "type": "articles", 
      "id": "1" 
    }, 
    { 
      "type": "articles", 
      "id": "2" 
    } 
  ]
}

Пустое состояние (null или []):

null — если запрашивался один ресурс, но он не найден (или связь пуста).

{
  "data": null
}

[] — если запрашивался список, но он пуст.

{
  "data": []
}

3.2 Структура 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"
    }
  }
}

4. Корневой ключ errors

Ключ errors (массив объектов ошибок) это обязательный ключ в тех случаях, когда запрос не может быть выполнен успешно.

4.1 Структура 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"
      }
    }
  ]
}

5. Корневой ключ meta

Корневой ключ meta это «свободный конверт» в JSON:API.

5.1 Структура meta

Спецификация никак не ограничивает его структуру, кроме одного условия: это должен быть объект. Он нужен для данных, которые не являются «ресурсами» (не имеют id и type).

{
  "meta": {
    "guid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "time": {
      "publish": 1697718264
    },
    "publisher": {
      "code": "system"
    }
  }
}

6. Корневой ключ jsonapi

Ключ jsonapi является опциональным параметром в корне контракта. Он используется для передачи информации о реализации спецификации сервером, что позволяет клиенту понять, какие возможности протокола доступны.

6.1 Структуры jsonapi

объект jsonapi может содержать следующие ключи:

  1. version (string): Указывает наивысшую версию спецификации JSON:API, которую поддерживает сервер.

  2. ext (array): Массив строк (URI), содержащий список всех примененных расширений (extensions). Расширения позволяют изменять базовое по��едение спецификации, и они должны поддерживаться и клиентом, и сервером.

  3. profile (array): Массив строк (URI), содержащий список примененных профилей. В отличие от расширений, профили предоставляют дополнительные соглашения, которые клиент может безопасно игнорировать, если не знает, как их обрабатывать.

  4. 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"
    }
  }
}

7. Корневой ключ links

Ключ links в корне документа JSON:API это набор ссылок, которые помогают клиенту «ориентироваться» в API, не вычисляя URL-адреса вручную. Это реализация концепции HATEOAS (Hypermedia as the Engine of Application State).

7.1 Структуры links

  1. Основные ссылки

    1. self: Ссылка на текущий запрос. Это URL, по которому был получен данный документ. Полезно для обновления данных (рефреша).

    2. related: Ссылка на связанные данные (если запрос был связан с отношениями).

  2. Ссылки пагинации (самые частые). Если данных много и они разбиты на страницы, используются следующие ключи:

    1. first: Ссылка на первую страницу данных.

    2. last: Ссылка на последнюю страницу данных.

    3. prev: Ссылка на предыдущую страницу (относительно текущей).

    4. 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 указывает на данные, которые относятся к текущему запросу, но технически являются «соседними» или производными.

7.2 Запрос связей (Relationship Link)

Самый частый случай использования 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, а все атрибуты автора (имя, биографию и т.д.).

7.3 Агрегированные данные или коллекции

Иногда 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 ведет обратно к статье, которой принадлежат эти комментарии. Это позволяет клиенту легко вернуться к родительскому объекту.

7.4 Формат ссылки

Ссылка в JSON:API может быть представлена двумя способами:

  1. Простая строка: Просто URL

"self": "https://api.example.com/articles/1"
  1. Объект ссылки: Если нужно передать дополнительные метаданные.

"self": {
  "href": "https://api.example.com/articles/1/pdf",
  "meta": {
    "contentType": "application/pdf",
    "fileSize": "2.4 MB"
  }
}

8. Корневой ключ included

Массив included является вспомогательным ключом корневого уровня, предназначенным для формирования составных документов. Его основная задача это оптимизация сетевого взаимодействия путем передачи связанных ресурсов в рамках одного HTTP-ответа.

8.1 Основные положения included

Согласно стандарту JSON:API, использование included регулируется следующими правилами:

  • В массив могут быть включены только те ресурсы, которые имеют хотя бы одну связь с первичными данными (data) или другими ресурсами внутри included. «Сиротские» (несвязанные) объекты не допускаются.

  • Массив included не должен присутствовать в документе, если в корне отсутствует ключ data.

  • Каждый объект в included является полноценным ресурсным объектом. Он обязан содержать ключи id и type, а также может включать attributesrelationshipslinks и 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"
      }
    }
  ]
}

9. Query-параметры

Вместо того чтобы изобретать велосипед в каждом новом контроллере, спецификация предлагает жесткий, но справедливый стандарт. Query-параметры здесь — это мощный декларативный язык запросов. Они позволяют клиенту точно определять структуру, объем и вложенность, минимизируя количество сетевых запросов и нагрузку на сервер.

9.1 Включение связанных данных (include)

Представьте экран в банковском приложении, где пользователь видит детали покупки. Нам нужно отобразить:

  1. Сумму и валюту (из самой Транзакции).

  2. Название и иконку магазина (из связанного Мерчанта).

  3. Последние 4 цифры карты (из связанного Счета/Карты).

  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 не будет данных.

9.2 Выбор полей (fields)

Если 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 всегда возвращаются по умолчанию, так как это «паспорт» ресурса.

10. Расширение: Atomic Operations

Atomic Operations (Атомарные операции) это официальное расширение JSON:API 1.1, которое позволяет клиенту группировать несколько действий (создание, обновление, удаление) в один HTTP-запрос и выполнять их как единую транзакцию. Atomic Operations решает две ключевые задачи: минимизацию количества сетевых запросов и обеспечение атомарности данных (принцип «все или ничего»). Если одна из операций в пакете завершается ошибкой, сервер обязан откатить все изменения, внесенные в рамках этого запроса.

Для активации расширения клиент и сервер должны использовать специфический Media Type в заголовках Content-Type и Acceptapplication/vnd.api+json;ext="https://jsonapi.org"

10.1 Структура контракта

В контрактах этого типа стандартные ключи data и included заменяются специфическими ключами расширения:

  • atomic:operations: Массив объектов, каждый из которых описывает одну операцию.

  • atomic:results: Массив результатов, возвращаемый сервером после успешного выполнения всех операций.

10.2 Типы операций

Каждый объект внутри atomic:operations должен содержать ключ op, определяющий тип действия:

add: Создание нового ресурса или добавление связи в коллекцию.

update: Обновление атрибутов или связей существующего ресурса.

remove: Удаление ресурса или разрыв связи.

10.3 Локальные идентификаторы

Критически важная особенность расширения это использование lid (Local ID). Это временный идентификатор, который позволяет ссылаться на ресурсы, создаваемые внутри этого же запроса.

Пример сценария: Создание автора и его книги одновременно.

  1. Первая операция создает автора и присваивает ему "lid": "new-author".

  2. Вторая операция создает книгу и в поле 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" 
            } 
          }
        }
      }
    }
  ]
}

11. Профиль: Cursor Pagination

Cursor Pagination (пагинация на основе курсоров) это наиболее производительный метод разделения данных на страницы. В отличие от классического метода offset (пропусти N строк), курсор не использует порядковый номер записи. Он опирается на указатель (курсор) на конкретный элемент, после которого нужно продолжить чтение.

11.1 Преимущества

  • Стабильность (No Drift): Если пока пользователь смотрит первую страницу, в начало списка добавят новую запись, при offset-пагинации данные «сдвинутся», и на второй странице пользователь увидит дубликат. Курсор же жестко привязан к элементу, поэтому дубликатов или пропусков не бывает.

  • Высокая производительность: Базе данных не нужно сканировать и отбрасывать тысячи строк, чтобы добраться до нужной (проблема OFFSET 1000000). С помощью курсора база делает эффективный прыжок по индексу сразу к нужной записи.

  • Идеально для бесконечных лент: Именно этот метод используют X, Slack и Twitter для бесконечной прокрутки.

11.2 Параметры

Когда применяется этот профиль, ссылки в объекте 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]: Ограничивает количество ресурсов на одной странице.