30
votes

I'm using FormRequest to validate from which is sent in an API call from my smartphone app. So, I want FormRequest alway return json when validation fail.

I saw the following source code of Laravel framework, the default behaviour of FormRequest is return json if reqeust is Ajax or wantJson.

//Illuminate\Foundation\Http\FormRequest class
/**
 * Get the proper failed validation response for the request.
 *
 * @param  array  $errors
 * @return \Symfony\Component\HttpFoundation\Response
 */
public function response(array $errors)
{
    if ($this->ajax() || $this->wantsJson()) {
        return new JsonResponse($errors, 422);
    }

    return $this->redirector->to($this->getRedirectUrl())
                                    ->withInput($this->except($this->dontFlash))
                                    ->withErrors($errors, $this->errorBag);
}

I knew that I can add Accept= application/json in request header. FormRequest will return json. But I want to provide an easier way to request my API by support json in default without setting any header. So, I tried to find some options to force FormRequest response json in Illuminate\Foundation\Http\FormRequest class. But I didn't find any options which are supported in default.

Solution 1 : Overwrite Request Abstract Class

I tried to overwrite my application request abstract class like followings:

<?php

namespace Laravel5Cg\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\JsonResponse;

abstract class Request extends FormRequest
{
    /**
     * Force response json type when validation fails
     * @var bool
     */
    protected $forceJsonResponse = false;

    /**
     * Get the proper failed validation response for the request.
     *
     * @param  array  $errors
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function response(array $errors)
    {
        if ($this->forceJsonResponse || $this->ajax() || $this->wantsJson()) {
            return new JsonResponse($errors, 422);
        }

        return $this->redirector->to($this->getRedirectUrl())
            ->withInput($this->except($this->dontFlash))
            ->withErrors($errors, $this->errorBag);
    }
}

I added protected $forceJsonResponse = false; to setting if we need to force response json or not. And, in each FormRequest which is extends from Request abstract class. I set that option.

Eg: I made an StoreBlogPostRequest and set $forceJsoResponse=true for this FormRequest and make it response json.

<?php

namespace Laravel5Cg\Http\Requests;

use Laravel5Cg\Http\Requests\Request;

class StoreBlogPostRequest extends Request
{

    /**
     * Force response json type when validation fails
     * @var bool
     */

     protected $forceJsonResponse = true;
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
        ];
    }
}

Solution 2: Add an Middleware and force change request header

I build a middleware like followings:

namespace Laravel5Cg\Http\Middleware;

use Closure;
use Symfony\Component\HttpFoundation\HeaderBag;

class AddJsonAcceptHeader
{
    /**
     * Add Json HTTP_ACCEPT header for an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $request->server->set('HTTP_ACCEPT', 'application/json');
        $request->headers = new HeaderBag($request->server->getHeaders());
        return $next($request);
    }
}

It 's work. But I wonder is this solutions good? And are there any Laravel Way to help me in this situation ?

5
Hi, just for a suggestion, since your asking for default response type, then why not just by adding a middleware and add request type to json in ur handle method $request->header('accept', 'application/json'); return $next($request); with these, you have a place to make further expansion, without always overriding any methodsterry low
Thanks ! It's a good idea. I think. I'll update this implementation in the question aboveChung
Sorry. I tried to set $request->header('Accept','application/json'); in a middleware but I found that my request has the default Accept header '/', so I couldn't overwrite that Accept header. I didn't set anything in my request.Chung
it doesnt matter, whether you have the default accept value set in your header, the middleware value will override it $request = $request->header('Accept','application/json'); return $next($request); im thinking that, the request is not being persisted.terry low
I found the way to overwrite request header, We need to set $request->server and rebuild headerBag like followings: $request->server->set('HTTP_ACCEPT', 'application/json'); $request->headers = new HeaderBag($request->server->getHeaders());Chung

5 Answers

34
votes

It boggles my mind why this is so hard to do in Laravel. In the end, based on your idea to override the Request class, I came up with this.

app/Http/Requests/ApiRequest.php

<?php

namespace App\Http\Requests;


class ApiRequest extends Request
{
    public function wantsJson()
    {
        return true;
    }
}

Then, in every controller just pass \App\Http\Requests\ApiRequest

public function index(ApiRequest $request)

33
votes

I know this post is kind of old but I just made a Middleware that replaces the "Accept" header of the request with "application/json". This makes the wantsJson() function return true when used. (This was tested in Laravel 5.2 but I think it works the same in 5.1)

Here's how you implement that :

  1. Create the file app/Http/Middleware/Jsonify.php

    namespace App\Http\Middleware;
    
    use Closure;
    
    class Jsonify
    {
    
        /**
         * Change the Request headers to accept "application/json" first
         * in order to make the wantsJson() function return true
         *
         * @param  \Illuminate\Http\Request  $request
         * @param  \Closure  $next
         * 
         * @return mixed
         */
        public function handle($request, Closure $next)
        {
            $request->headers->set('Accept', 'application/json');
    
            return $next($request);
        }
    }
    
  2. Add the middleware to your $routeMiddleware table of your app/Http/Kernel.php file

    protected $routeMiddleware = [
        'auth'       => \App\Http\Middleware\Authenticate::class,
        'guest'      => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'jsonify'    => \App\Http\Middleware\Jsonify::class
    ];
    
  3. Finally use it in your routes.php as you would with any middleware. In my case it looks like this :

    Route::group(['prefix' => 'api/v1', 'middleware' => ['jsonify']], function() {
        // Routes
    });
    
3
votes

Based on ZeroOne's response, if you're using Form Request validation you can override the failedValidation method to always return json in case of validation failure.

The good thing about this solution, is that you're not overriding all the responses to return json, but just the validation failures. So for all the other Php exceptions you'll still see the friendly Laravel error page.

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;

class InventoryRequest extends FormRequest
{
    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response($validator->errors(), Response::HTTP_UNPROCESSABLE_ENTITY));
    }
}
2
votes

if your request has either X-Request-With: XMLHttpRequest header or accept content type as application/json FormRequest will automatically return a json response containing the errors with a status of 422.

enter image description here

0
votes

i just override the failedValidation function

protected function failedValidation(Validator $validator)
{
    if ($this->wantsJson()) {
        throw new HttpResponseException(
            Response::error(__('api.validation_error'), 
            $validator->errors(), 
            470, 
            [], 
            new ValidationException)
        );
    }

    parent::failedValidation($validator);
}

So my custom output sample like below:

{
    "error": true,
    "message": "Validation Error",
    "reference": [
        "The device id field is required.",
        "The os version field is required.",
        "The apps version field is required."
    ],
}

BTW Response::error dont exist in laravel. Im using macroable to create new method

 Response::macro('error', function ($msg = 'Something went wrong', $reference = null, $code = 400, array $headers = [], $exception = null) {
      return response()->json(//custom here);
 });