skip to content

In Laravel, when do you use response()->json() instead of returning an array, and what do its status, headers and options arguments control?

level: middleimportance: should knowfreq 45%

answer

  1. status, headers and encoding flags
  2. json($data, $status, $headers, $options)
  3. options default 0: slashes come out escaped
  4. JSON_UNESCAPED_UNICODE, JSON_PRETTY_PRINT
  5. bad UTF-8 throws InvalidArgumentException

basics

~10 s

Returning an array gives JSON with status 200 and nothing else. response()->json($data, $status, $headers, $options) adds a chosen status, extra headers and json_encode flags, and returns an Illuminate\Http\JsonResponse you can keep chaining.

solid answer

~40 s

Returning an array is enough for a plain 200 JSON reply. You call `response()->json()` when the endpoint needs more: its signature is `json($data = [], $status = 200, array $headers = [], $options = 0)` and it returns an `Illuminate\Http\JsonResponse`. The status lets you answer 201 or 202, the headers array adds things like `Location` or `Retry-After`, and `$options` is passed straight to `json_encode`. Because Laravel's default is `0`, slashes come out as `\/` and non-ASCII text as `\u` escapes unless you pass `JSON_UNESCAPED_SLASHES` or `JSON_UNESCAPED_UNICODE`. If encoding fails, for example on invalid UTF-8, `setData()` throws an `InvalidArgumentException` instead of sending broken JSON. The result also supports `withHeaders()`, `cookie()` and `withCallback()` for JSONP.

code

php · 16 lines
php
<?php

use App\Models\Refund;
use Illuminate\Http\JsonResponse;

class RefundController
{
    public function store(Refund $refund): JsonResponse
    {
        $refund->queueForProcessing();

        return response()
            ->json(['refund' => $refund->id, 'state' => 'queued'], 202)
            ->withHeaders(['Retry-After' => 30]);
    }
}

go deeper

for a junior

Remember the argument order: data, status, headers, then json_encode options. Use it when you need a status other than 200 or an extra header.

for a middle

Explain how JsonResponse picks toJson, jsonSerialize or toArray, why the default flags escape slashes and Unicode, and that encoding errors throw InvalidArgumentException.

for a senior

Treat encoding failures as data-quality incidents, set flags deliberately for clients that compare raw bodies, and keep statuses and Location headers consistent across endpoints.

for a principal

Decide where JSON shape and status conventions are owned so that controllers do not each hand-roll response()->json() calls with drifting formats.

## Two ways to send JSON In Laravel you can produce a JSON reply without touching the response API at all: return an array, a model or a collection and the router wraps it in a `JsonResponse` with status 200. That covers most read endpoints. `response()->json()` exists for the cases where the automatic conversion is not enough. The signature on the response factory is: ```php public function json($data = [], $status = 200, array $headers = [], $options = 0) ``` It returns `new Illuminate\Http\JsonResponse($data, $status, $headers, $options)`. The class extends Symfony's `JsonResponse`, adds Laravel's `ResponseTrait` (so `header()`, `withHeaders()`, `cookie()` and `withCookie()` work) and is macroable. ## What each argument controls | Argument | Default | Typical use in a concert-venue API | |---|---|---| | `$data` | `[]` | an array, a model, a collection, anything `Jsonable`, `JsonSerializable` or `Arrayable` | | `$status` | `200` | `201` after booking a ticket, `202` when a refund is queued | | `$headers` | `[]` | `Location` of the new booking, `Retry-After`, `Cache-Control` | | `$options` | `0` | `JSON_UNESCAPED_UNICODE`, `JSON_UNESCAPED_SLASHES`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION` | ## How the data is encoded `JsonResponse::setData()` picks the encoder by type, in order: 1. `Jsonable` objects (Eloquent models, collections) call their own `toJson($options)`. 2. `JsonSerializable` objects are encoded from `jsonSerialize()`. 3. `Arrayable` objects are encoded from `toArray()`. 4. Anything else goes to `json_encode($data, $options)`. The practical consequences: - **Do not pre-encode.** Passing `json_encode($data)` hands the method a string, which is then encoded again into one JSON string literal full of escaped quotes. - **The flags matter.** With Laravel's default of `0`, a URL such as `https://tickets.example/42` is sent as `https:\/\/tickets.example\/42` and a venue named `Théâtre` arrives as `Th\u00e9\u00e2tre`. Both decode correctly, but people reading logs or comparing snapshots notice. Pass `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE` when you want the literal text. - **Floats keep their fraction only on request.** `JSON_PRESERVE_ZERO_FRACTION` sends `20.0` instead of `20`, which matters to typed clients. - **Encoding failures throw.** After encoding, `setData()` checks `json_last_error()`. Malformed UTF-8 (often from a legacy database column or a binary field) raises an `InvalidArgumentException` with PHP's error message, which surfaces as a 500 rather than a truncated body. The only escape hatch is `JSON_PARTIAL_OUTPUT_ON_ERROR`, which tolerates recursion, INF/NAN and unsupported types but not invalid UTF-8; fix the data or pass `JSON_INVALID_UTF8_SUBSTITUTE`. ## Chaining after the call Because the result is a Laravel response, you can keep building it: - `->withHeaders(['X-Venue' => 'north-hall'])` adds several headers at once. - `->header('Cache-Control', 'no-store')` adds or replaces one. - `->cookie('seat_hold', $holdId, 10)` attaches a cookie. - `->withCallback($request->input('callback'))` turns it into JSONP for an old widget. `response($array, 201)` is a close alternative: the plain `Response` class notices an array and encodes it with `application/json`, but it offers no encoding flags. ## A worked example ```php return response()->json( ['booking' => $booking->id, 'seat' => $booking->seat], 201, ['Location' => route('bookings.show', $booking)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ); ``` ## Returning an array versus calling `json()` | Code | Class | Status | Encoding flags | |---|---|---|---| | `return ['state' => 'queued'];` | `JsonResponse` | 200 only | none | | `return response(['state' => 'queued'], 202);` | `Response` with JSON body | any | none | | `return response()->json(['state' => 'queued'], 202, [...], JSON_UNESCAPED_UNICODE);` | `JsonResponse` | any | any | The bare array is the right default for simple reads. The moment a client depends on a specific status, a header or the exact byte form of the body, the explicit call documents that intent in the controller, and a reviewer can see it without knowing the router's conversion rules. Teams that return 201 from store actions usually standardise on `response()->json(..., 201)` rather than relying on the model special case. ## What a strong answer adds It keeps the argument order straight (status before headers, flags last), knows that Laravel's JSON is escaped by default, and treats the `InvalidArgumentException` on bad UTF-8 as a data problem to fix upstream. Formatting validation errors and exception bodies is a separate subject handled by the framework's exception rendering, not by this method.

  • A JSON endpoint starts failing with 'Malformed UTF-8 characters, possibly incorrectly encoded'. Where does that come from and what are the options?
    `JsonResponse::setData()` runs `json_encode` and throws `InvalidArgumentException` with `json_last_error_msg()` when encoding fails. Invalid UTF-8 usually comes from a column stored in another charset or binary data. Fix the data or connection charset, cast binary fields to base64, or pass `JSON_INVALID_UTF8_SUBSTITUTE` as the options argument to replace bad bytes.
  • Why do URLs in Laravel's JSON responses show up as `https:\/\/...`?
    `response()->json()` passes `$options = 0` to `json_encode`, and PHP escapes forward slashes by default. The JSON is still valid and decodes to the normal URL. Pass `JSON_UNESCAPED_SLASHES` in the fourth argument when readability of the raw body matters.

saying these in an interview costs you the question

  • You should json_encode the data before passing it to response()->json().
  • The third argument of response()->json() is the json_encode flags.
  • Invalid UTF-8 is silently dropped and the rest of the JSON is sent.
  • Returning an array can carry a custom status if the array has a status key.
  • response()->json() leaves forward slashes unescaped by default.