> ## Documentation Index
> Fetch the complete documentation index at: https://api.simkl.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Null and missing values

> Five different things a missing value can mean in the Simkl API, and how to handle each one.

`null` in a Simkl response is never noise — it's one of five specific signals. This page is the master reference; schema descriptions link back here by type.

<CardGroup cols={5}>
  <Card title="Type 1" icon="hourglass-half" href="#type-1">
    **Never happened yet** — event hasn't fired
  </Card>

  <Card title="Type 2" icon="ban" href="#type-2">
    **Doesn't apply** — key omitted entirely
  </Card>

  <Card title="Type 3" icon="folder-open" href="#type-3">
    **Empty result** — whole response null
  </Card>

  <Card title="Type 4" icon="circle-question" href="#type-4">
    **Unknown** — data not on file
  </Card>

  <Card title="Type 5" icon="flag-checkered" href="#type-5">
    **End state** — journey complete
  </Card>
</CardGroup>

<a id="type-1" />

## Type 1 — Never happened yet

<Tip>
  **Field present, value `null`** — the event hasn't fired for this user yet, but the field is a normal part of the model. Once it happens, the value flips to a real value and never reverts to `null`.
</Tip>

**Where you'll see it**

| Endpoint                                                      | Field                          | When it's null                              |
| ------------------------------------------------------------- | ------------------------------ | ------------------------------------------- |
| [`GET /sync/activities`](/api-reference/simkl/get-activities) | every bucket timestamp         | User has never had activity in that bucket  |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items)   | `last_watched_at`              | Plantowatch items — user hasn't watched yet |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items)   | `user_rated_at`, `user_rating` | Items the user hasn't rated                 |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items)   | `last_watched`                 | Shows the user hasn't started               |

**Handling**

<CodeGroup>
  ```js JavaScript {2,3} theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct
  if (item.last_watched_at !== null) {
    showLastWatched(item.last_watched_at);
  }

  // ✗ Wrong — gives a "watched on Jan 1 1970" UI
  showLastWatched(item.last_watched_at ?? '1970-01-01T00:00:00Z');
  ```

  ```python Python {2,3} theme={"theme":{"light":"github-light","dark":"vesper"}}
  # ✓ Correct
  if item.get('last_watched_at') is not None:
      show_last_watched(item['last_watched_at'])

  # ✗ Wrong
  show_last_watched(item.get('last_watched_at') or '1970-01-01')
  ```
</CodeGroup>

<a id="type-2" />

## Type 2 — Doesn't apply to this type

<Warning>
  **Key omitted entirely** — not set to `null`, the property doesn't exist on this object. Iterating keys won't return it; `'fieldName' in obj` returns `false`.
</Warning>

**Where you'll see it**

| Endpoint                                                                                           | Type                 | What's omitted                                                                                       |
| -------------------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------- |
| [`GET /sync/activities`](/api-reference/simkl/get-activities)                                      | `movies` block       | `watching` and `hold` keys (movies can't be in those statuses)                                       |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items)                                        | Movie records        | Episode-related fields (`seasons`, `last_watched`, etc.)                                             |
| Movie / TV records                                                                                 | —                    | Anime-only IDs (`mal`, `anidb`, `anilist`, `kitsu`) and `anime_type`                                 |
| [`GET /tv/episodes/{id}`](/api-reference/simkl/get-tv-episodes)                                    | TV specials          | `season` AND `episode` both omitted on `type: "special"` items                                       |
| [`GET /anime/episodes/{id}`](/api-reference/simkl/get-anime-episodes)                              | Every anime episode  | `season` omitted (AniDB numbers anime sequentially, no per-season concept)                           |
| [`GET /anime/{id}`](/api-reference/simkl/get-anime)                                                | TV-classified shows  | `relations` array omitted entirely (anime catalog only — TV records skip this field)                 |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) without `?memos=yes`                   | every entry          | `memo` key omitted (gated on the query param)                                                        |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) without `?next_watch_info=yes`         | every entry          | `next_to_watch_info` key omitted                                                                     |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) without `?allow_rewatch=yes`           | rewatch-session rows | `is_rewatch`, `rewatch_id`, `rewatch_status` keys omitted entirely; only the canonical entry returns |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) without `?extended=full_anime_seasons` | anime entries        | `mapped_tvdb_seasons` array omitted                                                                  |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) without `?episode_tvdb_id=yes`         | episode rows         | `tvdb` block on each episode omitted                                                                 |

See [Watchlist statuses](/conventions/list-statuses) for the full per-type matrix.

<Info>
  **`/sync/all-items/anime` entries wrap in `show:`, by design.** Each
  item in the `anime` array nests its media object under a key called
  `show:`, not `anime:`. This is intentional cross-catalog compatibility:
  apps that source their data from **TMDB or TVDB** treat anime as shows
  (those catalogs have no separate "anime" concept), so the `show:`
  wrapper keeps Simkl's sync responses drop-in compatible with code
  written against TMDB/TVDB-shaped data. Anime-only fields (`anime_type`,
  mal/anidb/anilist/kitsu IDs) sit alongside on the outer item and on
  `show.ids` respectively.

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct — same iteration pattern works for shows/, anime/, and
  //   future media types that follow the show-shaped TMDB/TVDB model
  const list = await fetch('/sync/all-items/anime').then(r => r.json());
  list.anime.forEach(item => {
    const metadata = item.show;          // anime use `show:` for the same
                                         // reason TVDB indexes anime under shows
    const ids = metadata.ids;            // ids.simkl, ids.mal, ids.anidb, ...
    const animeFormat = item.anime_type; // anime-specific outer-item field
  });
  ```
</Info>

**Handling**

<CodeGroup>
  ```js JavaScript {2,5} theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct
  if ('watching' in activities.tv_shows) {
    // TV has a watching bucket
  }
  // ✗ Wrong — activities.movies.watching is undefined, not null
  if (activities.movies.watching === null) {
    /* never true */
  }
  ```

  ```python Python {2,5} theme={"theme":{"light":"github-light","dark":"vesper"}}
  # ✓ Correct
  if 'watching' in activities['tv_shows']:
      pass

  # ✗ Wrong — KeyError
  ts = activities['movies']['watching']
  ```
</CodeGroup>

<a id="type-3" />

## Type 3 — Empty result set

<Note>
  **The whole response is `null` or an empty array** — the endpoint accepted the request but there are no items to return. The URL is valid; the call returns `200`; the body is just empty.
</Note>

**Where you'll see it**

| Endpoint                                                                    | When it's empty                                                                                       |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [`GET /sync/all-items/movies/watching`](/api-reference/simkl/get-all-items) | Always — movies can't have this status, the route is reachable but the data set is structurally empty |
| [`GET /sync/all-items/movies/hold`](/api-reference/simkl/get-all-items)     | Same                                                                                                  |
| [`GET /sync/all-items/{type}/{status}`](/api-reference/simkl/get-all-items) | Any bucket the user simply has no items in                                                            |
| [`POST /search/random`](/api-reference/simkl/search-random)                 | Returns `{"error": "not_found"}` when no item matches the filters                                     |

**Handling**

<CodeGroup>
  ```js JavaScript {3,8} theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct
  const items = await fetch('/sync/all-items/movies/watching').then(r => r.json());
  if (!items) return;        // empty bucket — bail early

  // ✗ Wrong — TypeError if response is null
  const items = await fetch(...).then(r => r.json());
  items.movies.forEach(...);
  ```

  ```python Python {3,7} theme={"theme":{"light":"github-light","dark":"vesper"}}
  # ✓ Correct
  data = r.json()
  if data is None:
      return

  # ✗ Wrong — AttributeError
  for m in r.json()['movies']:
      ...
  ```
</CodeGroup>

<a id="type-4" />

## Type 4 — Unknown / data not on file

<Info>
  **Field present, value `null`** — the data point is supposed to have a value but Simkl genuinely doesn't have it yet. Render a placeholder; don't display the literal `null`.
</Info>

**Where you'll see it**

| Endpoint                                                                                            | Field                | When it's null                                                                                         |
| --------------------------------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
| [`GET /tv/airing`](/api-reference/simkl/get-tv-airing)                                              | `date`               | Air date not on file yet                                                                               |
| [`GET /tv/episodes/{id}`](/api-reference/simkl/get-tv-episodes)                                     | `description`        | Episode synopsis unknown                                                                               |
| [`GET /tv/episodes/{id}`](/api-reference/simkl/get-tv-episodes)                                     | `img`                | Episode still not on file                                                                              |
| [`GET /movies/{id}`](/api-reference/simkl/get-movie)                                                | `ratings.imdb`, etc. | No external rating known                                                                               |
| [`GET /anime/{id}`](/api-reference/simkl/get-anime)                                                 | `en_title`           | Localized English title not on file (e.g. Death Note returns `null`)                                   |
| [`GET /tv/{id}`](/api-reference/simkl/get-tv-show), [`/anime/{id}`](/api-reference/simkl/get-anime) | `network`            | Mirrored from the upstream TVDB record. `null` when TVDB has no network on file (mostly recent anime). |
| [`GET /tv/{id}`](/api-reference/simkl/get-tv-show), [`/anime/{id}`](/api-reference/simkl/get-anime) | `airs`               | Recurring schedule not on file (older shows, anime films)                                              |
| [`GET /tv/{id}`](/api-reference/simkl/get-tv-show), [`/anime/{id}`](/api-reference/simkl/get-anime) | `status`             | Catalog `Status` column blank — rare; status is normally one of `tba`, `ended`, `airing`               |
| Catalog endpoints                                                                                   | `runtime`            | Runtime not tracked                                                                                    |

<Note>
  **`network` reflects the upstream catalog (TVDB).** The value is what
  TVDB classifies the record under — for streaming-only shows TVDB lists
  the platform as the "network" (`"Prime Video"`, `"Netflix"`, `"Apple TV+"`),
  for traditional broadcast it's the broadcaster (`"HBO"`, `"AMC"`, `"CBS"`).
  Don't infer distribution model from this field; treat it as an opaque
  display string from the source catalog.
</Note>

<Tip>
  **`null` vs empty string.** Older Simkl records sometimes return an empty
  string `""` for a field rather than `null` (e.g. `en_title: ""` on classic
  anime, before the API standardized on `null`). Both mean Type 4 — data
  not on file. Check both:

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const englishTitle = item.en_title || 'No English title on file';
  // matches null AND empty string
  ```
</Tip>

<Note>
  **`memo: {}` is a Type 4 variant.** On
  [`GET /sync/all-items?memos=yes`](/api-reference/simkl/get-all-items),
  items the user has set a memo on return:

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  { "memo": { "text": "Loved this season", "is_private": false } }
  ```

  Items without a memo return an **empty object**:

  ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
  { "memo": {} }
  ```

  NOT `null`, NOT missing — `{}`. The empty-object marker keeps the key
  shape consistent (client code can always do `item.memo.text || ''`
  without an existence check or a null-coalesce). When `?memos=yes` is
  NOT set, the `memo` key is omitted entirely (Type 2).
</Note>

**Handling**

<CodeGroup>
  ```jsx JSX {2,5} theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct
  <span>{episode.description ?? 'No description yet.'}</span>

  // ✓ Correct — guard date-display code
  <span>{episode.date ? formatDate(episode.date) : 'TBA'}</span>
  ```

  ```python Python {2} theme={"theme":{"light":"github-light","dark":"vesper"}}
  # ✓ Correct
  runtime_str = f"{movie['runtime']} min" if movie.get('runtime') else "—"
  ```
</CodeGroup>

<a id="type-5" />

## Type 5 — End state reached

<Check>
  **Field present, value `null`** — the value used to be meaningful but the workflow has ended; the field is intentionally cleared. Render the "complete" state in your UI.
</Check>

**Where you'll see it**

| Endpoint                                                                           | Field                | When it's null                            |
| ---------------------------------------------------------------------------------- | -------------------- | ----------------------------------------- |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items)                        | `next_to_watch`      | Show in `completed` — no further episodes |
| [`GET /sync/all-items`](/api-reference/simkl/get-all-items) `?next_watch_info=yes` | `next_to_watch_info` | Same — completed show, nothing remaining  |

**Handling**

<CodeGroup>
  ```jsx JSX {2-4} theme={"theme":{"light":"github-light","dark":"vesper"}}
  // ✓ Correct
  {show.next_to_watch
    ? <Badge>Next: {show.next_to_watch}</Badge>
    : <Badge variant="success">All caught up</Badge>}
  ```

  ```python Python {1,3} theme={"theme":{"light":"github-light","dark":"vesper"}}
  if show.get('next_to_watch'):
      print(f"Next: {show['next_to_watch']}")
  else:
      print("All caught up")
  ```
</CodeGroup>

## Empty / Missing response shape — by endpoint

The API uses **five different shapes** for "this request was valid but
there's no data". The table below tells you what to expect at each
endpoint so a single parsing path can handle them all.

### The five shapes you'll meet

<CardGroup cols={5}>
  <Card title="200 [ ]" icon="brackets-square">
    Empty array. Safe to iterate.
  </Card>

  <Card title="200 null" icon="circle-dashed">
    Top-level `null`. `r.json()` returns `None`.
  </Card>

  <Card title="200 { }" icon="ban">
    Empty object. `len()` is 0.
  </Card>

  <Card title="301 redirect" icon="right-from-bracket">
    Answer in `Location:` header. No body.
  </Card>

  <Card title="200 fallthrough" icon="shuffle">
    Server returned unrelated discover data instead of an error.
  </Card>
</CardGroup>

<Warning>
  **Status-code lies.** Five endpoints return `200 OK` with an empty
  shape rather than `404 Not Found`. Branch on the body shape, not just
  the status — see the rows tagged in the table below.

  Conversely, three CDN endpoints (`/calendar/...`, `/discover/...`)
  return `404` with a **plain string body**, not the JSON error envelope
  the rest of the API uses. `r.json()` will raise on these — wrap the
  parse.
</Warning>

### Per-endpoint shape matrix

| Endpoint                                           | Trigger                                       |  Status | Body shape                                             | How to handle                                                                                                              |
| -------------------------------------------------- | --------------------------------------------- | :-----: | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `GET /movies/{id}`                                 | unknown numeric Simkl ID                      | **200** | `[]`                                                   | Empty array (Type 3). Safe to iterate.                                                                                     |
| `GET /tv/{id}`                                     | unknown numeric Simkl ID                      | **200** | `[]`                                                   | Same.                                                                                                                      |
| `GET /anime/{id}`                                  | unknown numeric Simkl ID                      | **200** | `[]`                                                   | Same.                                                                                                                      |
| `GET /tv/episodes/{id}`                            | unknown parent show ID                        | **200** | `[]`                                                   | Same.                                                                                                                      |
| `GET /anime/episodes/{id}`                         | unknown parent anime ID                       | **200** | `[]`                                                   | Same.                                                                                                                      |
| `GET /tv/episodes/{id}`                            | non-numeric `id`                              | **200** | `[]`                                                   | Same. **No 404** for non-numeric id.                                                                                       |
| `GET /anime/episodes/{id}`                         | non-numeric `id`                              | **200** | `[]`                                                   | Same.                                                                                                                      |
| `GET /movies/{id}`                                 | non-numeric `id`                              |   404   | `{error, code, message?}`                              | JSON error envelope.                                                                                                       |
| `GET /tv/{id}`                                     | non-numeric `id`                              | **200** | `object{...}`                                          | **Fallthrough** — server returns discover data (`top_aired_fanarts`, etc.) instead of an error — NOT a `ShowDetail` shape. |
| `GET /anime/{id}`                                  | non-numeric `id`                              | **200** | `object{...}`                                          | Same fallthrough behavior.                                                                                                 |
| `GET /tv/episodes/`                                | empty `id` (trailing slash)                   |   400   | `{error: "empty_id", code: 400}`                       | JSON error envelope.                                                                                                       |
| `GET /anime/episodes/`                             | empty `id`                                    |   400   | `{error: "empty_id", code: 400}`                       | Same.                                                                                                                      |
| `GET /sync/activities`                             | fresh account (no activity yet)               |   200   | `object{}` w/ all-null timestamps                      | Type 1 nulls inside, never null at top.                                                                                    |
| `GET /sync/all-items`                              | brand-new user (no library, no `type` filter) |   200   | `{}`                                                   | Empty object. Iterating keys yields nothing.                                                                               |
| `GET /sync/all-items/{type}`                       | empty bucket                                  |   200   | `object{type: []}`                                     | Object wrapper present; inner array is empty.                                                                              |
| `GET /sync/all-items/movies/watching`              | movies can't be `watching`                    | **200** | `{}`                                                   | **Empty object** — not `null`, not `[]`. Iterating keys yields nothing.                                                    |
| `GET /sync/all-items/movies/hold`                  | movies can't be `hold`                        | **200** | `{}`                                                   | Same.                                                                                                                      |
| `GET /sync/all-items?memos=yes`                    | item with no memo set                         | **200** | `{ "memo": {} }` per item                              | Empty object inside the per-item shape — NOT null, NOT missing. Type 4 variant.                                            |
| `GET /sync/all-items/badtype`                      | bad `type` segment                            | **200** | `object{...}`                                          | **Fallthrough** — server returns a default response with cross-type data. **No 400/404**.                                  |
| `GET /sync/all-items/{type}/badstatus`             | bad `status` segment                          |   200   | `object{type: []}`                                     | Falls back to "all of that type", just empty. **No 400/404**.                                                              |
| `GET /sync/ratings/{type}/{1-10}`                  | user has no ratings at this score             | **200** | `{}`                                                   | Empty object, NOT empty array.                                                                                             |
| `GET /sync/ratings/{type}/0`                       | rating \< 1 (out of range)                    | **200** | `{}`                                                   | Same — no validation error.                                                                                                |
| `GET /sync/ratings/{type}/11`                      | rating > 10 (out of range)                    | **200** | `{}`                                                   | Same.                                                                                                                      |
| `GET /sync/ratings/badtype/{N}`                    | bad `type`                                    | **200** | `{}`                                                   | Same.                                                                                                                      |
| `GET /sync/playback/{type}`                        | empty                                         |   200   | `array[...]`                                           | Returns array (possibly empty).                                                                                            |
| `GET /sync/playback/badtype`                       | bad `type`                                    |   404   | `{error: "url_failed", code: 404}`                     | **404 here, but 200 fallthrough on /sync/all-items/badtype** — be aware of the asymmetry.                                  |
| `GET /search/{type}`                               | missing `q` parameter                         | **200** | `[]`                                                   | Empty array.                                                                                                               |
| `GET /search/badtype`                              | bad type                                      | **200** | `null`                                                 | **Top-level null.** `r.json()` returns `None`.                                                                             |
| `GET /search/id`                                   | unknown external id                           | **200** | `[]`                                                   | Empty array.                                                                                                               |
| `GET /search/id`                                   | no id param at all                            | **200** | `[]`                                                   | Same.                                                                                                                      |
| `POST /search/random`                              | empty body                                    | **200** | `object{...}`                                          | Returns a random item — never empty.                                                                                       |
| `POST /search/random`                              | filters yield no match                        | **200** | `{error: "not_found", ...}`                            | **Status is 200 but body is the error envelope.** Branch on `'error' in body`, not on status.                              |
| `POST /search/file`                                | empty body                                    | **200** | `null`                                                 | **Top-level null.**                                                                                                        |
| `GET /ratings`                                     | bad params, no id                             | **200** | `null`                                                 | **Top-level null.**                                                                                                        |
| `GET /ratings`                                     | unknown imdb id                               | **200** | `null`                                                 | Same.                                                                                                                      |
| `GET /ratings/{type}`                              | missing `user_watchlist` param                | **200** | `null`                                                 | Same — even WITHOUT a Bearer token (auth gate is on the param).                                                            |
| `POST /users/settings`                             | normal                                        |   200   | `object{user, account, connections}`                   | Always a populated object for the authed user.                                                                             |
| `POST /users/{user_id}/stats`                      | unknown `user_id`                             | **200** | `object{...}` w/ zero stats                            | Returns a zero-counts record, NOT 404. (Cross-account access intentional.)                                                 |
| `GET /users/recently-watched-background/{user_id}` | numeric unknown user                          | **200** | `null`                                                 | **Top-level null** for a numeric-but-unknown id.                                                                           |
| `GET /users/recently-watched-background/{user_id}` | non-numeric user\_id                          |   404   | `{error: "user_id_failed", code: 404}`                 | JSON error envelope.                                                                                                       |
| `GET /redirect`                                    | no id at all                                  | **301** | empty body, `Location: //simkl.com`                    | **301 redirect** — body is empty; answer is in the header. Don't `r.json()`.                                               |
| `GET /redirect`                                    | unknown external id                           | **301** | empty body, `Location: //simkl.com`                    | Same — fallback to simkl.com root.                                                                                         |
| `GET /redirect`                                    | valid id                                      | **301** | empty body, `Location: //simkl.com/<type>/<id>/<slug>` | Always 301 — never returns JSON.                                                                                           |
| `GET /oauth/pin/{user_code}`                       | unknown `user_code`                           |   200   | `object{...}` (pending status)                         | Returns the pending-PIN shape, NOT 404.                                                                                    |
| `GET /calendar/{type}.json` *(CDN)*                | normal                                        |   200   | `array[...]`                                           | —                                                                                                                          |
| `GET /calendar/{year}/{month}/{type}.json` *(CDN)* | far-future month                              | **404** | **plain string body**                                  | **`r.json()` will raise.** Wrap the parse.                                                                                 |
| `GET /calendar/badtype.json` *(CDN)*               | bad type                                      | **404** | **plain string body**                                  | Same.                                                                                                                      |
| `GET /discover/trending/{file}.json` *(CDN)*       | normal                                        |   200   | `array[...]` or `object{...}` (combined)               | —                                                                                                                          |
| `GET /discover/trending/badcombo.json` *(CDN)*     | unknown filename                              | **404** | **plain string body**                                  | Same — CDN errors are not JSON.                                                                                            |

### Defensive parser pattern

If you're writing a generic Simkl client wrapper, this defensive
pattern handles every shape above:

<CodeGroup>
  ```js JavaScript theme={"theme":{"light":"github-light","dark":"vesper"}}
  async function safeJson(response) {
    // CDN 404s and the redirect-without-follow are not JSON.
    if (response.status >= 300 && response.status < 400) return null;
    if (!response.headers.get('content-type')?.includes('json')) return null;
    let body;
    try { body = await response.json(); }
    catch { return null; }                     // bad-content-type 404 string
    if (body === null) return null;            // top-level null (Type 3)
    if (Array.isArray(body) && body.length === 0) return [];   // empty array
    if (typeof body === 'object' &&
        Object.keys(body).length === 0) return {};             // empty object
    if (body && typeof body === 'object' &&
        'error' in body && response.status === 200) {
      // 200-with-error-envelope (e.g. POST /search/random with no match)
      return null;
    }
    return body;
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vesper"}}
  def safe_json(r):
      """Returns the parsed body or None for any "no useful data" shape."""
      if 300 <= r.status_code < 400:
          return None                                      # redirect — header has the answer
      ct = (r.headers.get("content-type") or "").lower()
      if "json" not in ct:
          return None                                      # CDN 404 string body
      try:
          body = r.json()
      except ValueError:
          return None
      if body is None:
          return None                                      # top-level null
      if isinstance(body, list) and not body:
          return []                                        # empty array
      if isinstance(body, dict):
          if not body:
              return {}                                    # empty object
          if r.status_code == 200 and "error" in body:
              return None                                  # 200-with-error envelope
      return body
  ```
</CodeGroup>

### Pinning a shape in your code

If your integration depends on a specific row of this table, pin it as
a comment in your code. When the table changes on a future doc release,
your in-code reference makes the diff easier to spot during a routine
dependency-update review.

## Quick decision table

| Symptom                          |  Type | Means                 | Do                                     |
| -------------------------------- | :---: | --------------------- | -------------------------------------- |
| Timestamp `null` on activities   | **1** | Never happened yet    | Don't show "last activity"             |
| Key isn't in the object          | **2** | Doesn't apply to type | `'key' in obj` check, or skip silently |
| Whole response is `null`         | **3** | Bucket is empty       | Render empty state                     |
| Description/image is `null`      | **4** | Data not on file      | Render placeholder ("TBA", "—")        |
| `next_to_watch` `null` on a show | **5** | Show is completed     | Render "all caught up"                 |

## Related

<CardGroup cols={2}>
  <Card title="Watchlist statuses" icon="list-check" href="/conventions/list-statuses">
    Which statuses are valid per type. The reason `watching` and `hold` are omitted from movie blocks (Type 2).
  </Card>

  <Card title="Standard media objects" icon="film" href="/conventions/standard-media-objects">
    Field-by-field shape of Movie / Show / Anime / Episode — which fields can be null, which are always present.
  </Card>

  <Card title="Dates and timezones" icon="clock" href="/conventions/dates">
    ISO-8601, the `Z` suffix, and the `1970-01-01T00:00:01Z` "very long time ago" placeholder.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/conventions/errors">
    How error envelopes differ from null responses, and when you'll see each.
  </Card>
</CardGroup>
