skip to content

In Laravel, what does JsonResource::withoutWrapping() change for a mobile API, and which resource responses still arrive wrapped in a data key?

level: seniorimportance: should knowfreq 32%

answer

  1. static $wrap defaults to 'data'
  2. only the outermost resource is wrapped
  3. links and meta force a data key
  4. an existing data key skips wrapping
  5. paginationInformation($request, $paginated, $default)

basics

~10 s

withoutWrapping() sets the static JsonResource::$wrap to null, so a plain outermost resource is sent bare. Paginated collections, responses with with() or additional() data, and data keys you write yourself still carry a data key.

solid answer

~40 s

By default the outermost resource is wrapped under `data`, because `JsonResource::$wrap` is `'data'`; nested resources are never wrapped. Calling `JsonResource::withoutWrapping()` in `AppServiceProvider::boot()` sets that static to `null` for the whole app, so `new WorkoutResource($w)` returns a bare object. Wrapping still happens when there is anything to put beside the payload: a paginated collection adds `links` and `meta`, and non-empty `with()` or `additional()` data forces a `data` key too. A `data` key you return from a custom collection's `toArray()` is not removed. The reverse trap: if a resource's own array already contains a `data` key, the wrapper is skipped, unless `JsonResource::$forceWrapping` is set to true. Pagination output is customised with `paginationInformation()` on a collection class, and `preserveQuery()` keeps filters in the page links.

code

php · 20 lines
php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class WorkoutCollection extends ResourceCollection
{
    public function paginationInformation($request, $paginated, $default): array
    {
        // Mobile clients follow next/prev and read the total; drop numbered page links.
        unset($default['meta']['links']);
        $default['meta']['units'] = 'metric';

        return $default;
    }
}

// Controller:
// return (new WorkoutCollection(Workout::latest()->paginate(20)))->preserveQuery();

go deeper

for a junior

Know that resource responses are wrapped in data by default and that JsonResource::withoutWrapping() in a service provider turns this off.

for a middle

Explain that $wrap is static and global, that only the outermost resource is wrapped, and what links and meta a paginated collection adds.

for a senior

Predict the shapes that stay wrapped (paginators, with, additional), catch the data-column trap and forceWrapping, and use paginationInformation and preserveQuery to give mobile clients one consistent, filter-preserving envelope.

for a principal

Set the envelope as an API-wide standard: one top-level shape for single items, lists and pages, and a plan for changing it without breaking app versions already installed on phones.

## Wrapping by default When a controller returns an API resource (a class extending `JsonResource`) or a resource collection, Laravel builds the response through a `ResourceResponse`. Its `wrap()` step puts the **outermost** resolved array under a key taken from the resource class's static `$wrap` property, which defaults to `'data'`. Resources nested inside another resource's array are serialized in place and never get their own `data` key, so there is no double wrapping. ```json {"data": {"id": 7, "type": "run", "distance_km": 10.2}} ``` The wrapper gives every response the same top level, which leaves room for siblings such as `meta` and `links` without mixing them into the payload. ## What withoutWrapping() changes `JsonResource::withoutWrapping()` sets `JsonResource::$wrap = null`. Because `$wrap` is a **static** property, the change is global to the application: it is normally called once in a service provider's `boot()` method. `JsonResource::wrap('workout')` sets a different key globally, and a subclass can redeclare `public static $wrap = 'workout';` to change only its own responses; a subclass that redeclares `$wrap` also keeps its own value when `withoutWrapping()` is called on `JsonResource`. After `withoutWrapping()`, `new WorkoutResource($w)` returns the fields at the top level and `WorkoutResource::collection(Workout::all())` returns a bare JSON array. ## Responses that stay wrapped The wrapping decision checks more than `$wrap`: 1. **Paginated collections.** Passing a paginator to `WorkoutResource::collection()` (or a custom collection) produces a `PaginatedResourceResponse`, which adds `links` and `meta`. Because there is extra information to place beside the items, the items go under `data` even when `$wrap` is null. 2. **`with()` and `additional()` data.** If either returns a non-empty array and the payload is unwrapped, the response wraps it as `data` so the extra keys have somewhere to sit. With wrapping disabled the key is still `data`. 3. **Your own `data` keys.** If a custom collection's `toArray()` returns `['data' => $this->collection, ...]`, that key is part of your payload; `withoutWrapping()` only concerns the automatic outer wrapper. | Response | Default wrapping | After `withoutWrapping()` | |---|---|---| | Single resource | `{"data": {...}}` | `{...}` | | `::collection($all)` | `{"data": [...]}` | `[...]` | | `::collection($paginator)` | `data`, `links`, `meta` | still `data`, `links`, `meta` | | Resource with `additional()` | `data` plus extra keys | still `data` plus extra keys | ## The mirror-image trap: a payload that already has data The wrapper is applied only when the resolved array does **not** already contain the wrapper key. Suppose a `Workout` stores raw sensor output in a JSON column named `data` and the resource exposes it as `'data' => $this->data`. The resolved array now contains `data`, so Laravel assumes it is already wrapped and sends `{"id": 7, "data": {...}}` with no outer wrapper, while every other endpoint is wrapped. Mobile decoders then fail on one endpoint only. Fixes: rename the key (`'sensor_data'`), or set `JsonResource::$forceWrapping = true`, a public static flag that makes the response wrap whenever a wrapper key is configured. ## Pagination links and meta For a length-aware paginator, `links` holds `first`, `last`, `prev` and `next` URLs, and `meta` holds the rest of the paginator's array: `current_page`, `from`, `last_page`, `links` (the numbered page links), `path`, `per_page`, `to` and `total`. Simple and cursor paginators supply different fields; their mechanics belong to the pagination topic. Two hooks shape this output: - **`paginationInformation($request, $paginated, $default)`**: a method on a `ResourceCollection` subclass. `$default` holds the `links` and `meta` arrays Laravel built; return the array you want instead, for example to drop the numbered page links that a mobile client never renders. The anonymous collection from `::collection()` has no such method (short of registering a global macro), which is one reason to write a `WorkoutCollection`. - **`preserveQuery()` or `withQuery([...])`** on the collection: copy the current query string, or a chosen array, into every page link, so `?type=run` survives when the app follows `next`. Keys from `with()` and `additional()` are merged recursively with the pagination arrays, so a `meta` entry you add sits beside `current_page` rather than replacing it. ## What to decide for a mobile API - **Pick one shape and apply it everywhere.** Disabling wrapping for single resources while paginated lists remain wrapped gives clients two top-level shapes. - **Keep `data` out of field names** unless `forceWrapping` is on. - **Treat the static as global state**: tests or long-running workers that toggle `$wrap` affect later responses in the same process.

  • Why does a nested WorkoutResource inside an AthleteResource never get its own data key?
    Wrapping happens in `ResourceResponse`, which only the outermost resource builds when it is converted to a response. Nested resources are serialized through `jsonSerialize()` and `resolve()`, which return the plain array. That is also why `with()` on a nested resource is ignored: only the outermost resource contributes top-level data.
  • How would you make only WorkoutResource responses use a workout key instead of data?
    Redeclare the static in the subclass: `public static $wrap = 'workout';`. The response reads `$wrap` from the concrete resource class, so only that class changes. Calling `WorkoutResource::wrap('workout')` without the redeclaration would assign the static inherited from `JsonResource` and change every resource.
  • A paginated response must drop the numbered page links from meta. Where do you do it?
    Write a `ResourceCollection` subclass with `paginationInformation($request, $paginated, $default)`, unset `$default['meta']['links']`, and return `$default`. The paginated response checks the collection instance for that method, so an anonymous `WorkoutResource::collection()` cannot carry it short of a global macro.

saying these in an interview costs you the question

  • withoutWrapping() also unwraps paginated resource collections.
  • withoutWrapping() only affects the resource class you call it on.
  • Nested resources get their own data key unless wrapping is disabled.
  • A resource field named data is always wrapped again under an outer data key.
  • additional() meta is placed at the top level beside unwrapped fields.
  • paginationInformation() belongs on the single-item WorkoutResource class.