---
name: json-api-envelope
source: https://app.decimal.ai/s/json-api-envelope@1/SKILL.md
source_sha256: 134d7677becf
---

# JSON:API document envelope

## Contract

Every JSON document the service sends or accepts is a JSON:API (jsonapi.org, v1.1) document:
primary resources under a top-level `data` member, failures under a top-level `errors` array
(never both in one document), extras under `meta`, resource objects of `type` + `id` +
`attributes` + `relationships`, and the `application/vnd.api+json` media type. Apply when
shaping request/response bodies for a resource-style HTTP service; not for RFC 9457
problem-details bodies (a separate convention) and not when consuming a format you don't
control.

## Rules

1. **Top-level members.** A document carries at least one of `data`, `errors`, `meta` — and
   `data` and `errors` never coexist. Nothing else wraps them: no `{"success": true}` flag, no
   `{"result": ...}` or `{"payload": ...}` nesting, no collection-named top-level key.

2. **`data` holds the primary resource(s).** One resource object for a single-resource
   document, an array of resource objects for a collection, `null` for an absent to-one
   target, `[]` for an empty collection. On a success document `data` is present, never
   silently omitted.

3. **Resource-object shape.** Exactly this frame:

   ```json
   { "type": "articles", "id": "1",
     "attributes": { },
     "relationships": { } }
   ```

   - `type` — the collection name, a string.
   - `id` — ALWAYS a JSON string, even when the underlying key is an integer.
   - `attributes` — the record's own fields; `type` and `id` never appear inside it.
   - `relationships` — one entry per connection to another resource.

4. **Foreign keys become relationships.** A reference to another record is never a bare
   `author_id`-style attribute; it is a relationship whose `data` is a resource identifier:

   ```json
   "relationships": { "author": { "data": { "type": "people", "id": "9" } } }
   ```

   To-many linkage is an array of `{type, id}` identifiers; an empty to-one is `"data": null`;
   an empty to-many is `"data": []`.

5. **Compound documents.** When related records ride along, their full resource objects go in
   the top-level `included` array, and the primary resources point at them through
   `relationships` by `{type, id}` — related records are never inlined inside `attributes`.

6. **Errors.** `errors` is a top-level ARRAY of error objects with members like `status` (the
   HTTP code as a string), `title` (a short type-level summary), `detail`
   (occurrence-specific), and `source`. Never a single error object, never a bare `message`
   string, never beside `data`.

7. **`meta`.** Non-standard extras — page counts, request ids, timings — go under top-level
   `meta` (or a resource-level `meta`), never as loose top-level fields like `total` or
   `page`.

8. **Media type.** Documents travel as `Content-Type: application/vnd.api+json`, and clients
   ask for them with the same value in `Accept`. Plain `application/json` is not the
   convention.

## Worked examples

Collection with a reference — the ad-hoc default, then the conforming document:

```json
BEFORE
{ "articles": [ { "id": 1, "title": "Caching basics", "author_id": 9 } ], "total": 1 }
```

```json
AFTER
{
  "data": [
    { "type": "articles", "id": "1",
      "attributes": { "title": "Caching basics" },
      "relationships": { "author": { "data": { "type": "people", "id": "9" } } } }
  ],
  "meta": { "total": 1 }
}
```

Compound document — related record inlined vs. placed in `included`:

```json
BEFORE
{ "article": { "id": 1, "title": "Caching basics",
               "author": { "id": 9, "name": "R. Ortiz" } } }
```

```json
AFTER
{
  "data": { "type": "articles", "id": "1",
            "attributes": { "title": "Caching basics" },
            "relationships": { "author": { "data": { "type": "people", "id": "9" } } } },
  "included": [
    { "type": "people", "id": "9", "attributes": { "name": "R. Ortiz" } }
  ]
}
```

Failure — ad-hoc flag vs. the errors array:

```json
BEFORE
{ "success": false, "error": "Article not found" }
```

```json
AFTER
{ "errors": [ { "status": "404", "title": "Not Found",
                "detail": "No article numbered 7 exists." } ] }
```

Creation request — flat fields vs. a typed resource object:

```json
BEFORE
{ "title": "Caching basics", "author_id": 9 }
```

```json
AFTER
{ "data": { "type": "articles",
            "attributes": { "title": "Caching basics" },
            "relationships": { "author": { "data": { "type": "people", "id": "9" } } } } }
```

## Edge cases & exceptions

- **Empty collection** → `"data": []`, not `null`, not an omitted member, not an error.
- **Absent to-one target** → the relationship stays, with `"data": null` — not a
  `..._id: null` attribute and not a missing key.
- **Creation** → the request document's resource object may omit `id`; the response document
  supplies the authoritative string `id`.
- **Integer database keys** → serialized as strings anyway (`"7"`, not `7`).
- **`meta` with `data`** is fine; `meta` with `errors` is fine; `data` with `errors` never.
- **Pagination** → page links (`first`, `prev`, `next`, `last`) under top-level `links`;
  counts under `meta`.
- **A field literally named `type` or `id`** → it cannot live in `attributes` (reserved);
  rename it (e.g. `kind`) or nest it.

## Do / Don't

- Do wrap primary resources in `data`. Don't emit a collection-named top-level key.
- Do keep the record's fields inside `attributes`. Don't flatten them onto the resource
  object.
- Do link records under `relationships` with `{type, id}` identifiers. Don't ship `*_id`
  attributes.
- Do carry full related records in `included`. Don't inline them inside `attributes`.
- Do report failures as an `errors` array. Don't return `{"success": false}` or mix errors
  with data.
- Do serialize every `id` as a string. Don't leak integer ids.
- Do declare `application/vnd.api+json`. Don't default to `application/json`.

## Common mistakes

- `{"users": [...]}` / `{"items": [...]}` — the collection-named wrapper instead of `data`.
- `{"success": true, "data": ...}` — a boolean status flag the envelope never uses.
- Numeric `id` values copied straight from the database.
- Foreign keys (`customer_id`, `owner_id`) left inside `attributes`.
- A related record nested whole inside the parent's `attributes` instead of `included`.
- One error object (or a bare string) instead of the `errors` array; errors returned beside
  `data`.
- `total` / `page` / `request_id` floating at the top level instead of under `meta`.
- `Content-Type: application/json` on a conforming document.

## Quick checklist

- Top level: only `data` / `errors` / `meta` (plus `links`, `included`); never `data` with
  `errors`.
- Every resource object: string `type`, string `id`, fields in `attributes`.
- Every reference: a `relationships` entry with `{type, id}` linkage — no `*_id` attributes.
- Related records: full objects in `included`, pointed at by `{type, id}`.
- Failures: `errors` array of `status`/`title`/`detail` objects.
- Extras: under `meta`. Media type: `application/vnd.api+json`.
