Fosrestbundle: Detailed form errors

Created on 20 Aug 2014  路  22Comments  路  Source: FriendsOfSymfony/FOSRestBundle

At the moment the form errors look something like:

{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "children": {
      "username": {
        "errors": [
          "This value should not be blank."
        ]
      }
    }
  }
}

I would like to have both error message and error code for each validated form field, so the json would look like this example below http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#errors

{
  "code" : 1024,
  "message" : "Validation Failed",
  "errors" : [
    {
      "code" : 5432,
      "field" : "first_name",
      "message" : "First name cannot have fancy characters"
    },
    {
       "code" : 5622,
       "field" : "password",
       "message" : "Password cannot be blank"
    }
  ]
}

Is it possible to obtain this with Symfony? Is it possible to associate an error code to a validation constraint? Or do you see any other solution?

Most helpful comment

@eXtreme

I had similar issue and solved it with ExceptionWrapperHandler:

class ExceptionWrapperHandler implements ExceptionWrapperHandlerInterface
{

    /**
     * @param array $data
     *
     * @return ExceptionWrapper
     */
    public function wrap($data)
    {
        if ($data['errors'] instanceof Form) {
            $form = $data['errors'];
            $data['errors'] = [];
            foreach ($form->getErrors(true, true) as $error) {
                $path = $error->getCause()->getPropertyPath();
                $data['errors'][$path] = $error->getMessage();
            }
        }
        return new ExceptionWrapper($data);
    }

}

In my Controller I simply return Form if it's not valid:

class UserController extends FOSRestController
{

    /**
     * @Post("/register")
     *
     * @param Request $request
     * @return array
     */
    public function registerAction(Request $request)
    {
        $form = $this->container->get('fos_user.registration.form');
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->container->get('fos_user.user_manager');

        $user = $userManager->createUser();
        $form->setData($user);

        $form->handleRequest($request);

        if (!$form->isValid()) {
            return $form;
        }

        $user->setEnabled(true);
        $userManager->updateUser($user);

        return $user;
    }

}

Example result:

{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "data.username": "Ta nazwa u\u017cytkownika jest ju\u017c zaj\u0119ta",
    "data.email": "Podany email jest zaj\u0119ty"
  }
}

All 22 comments

the responsible code is https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Util/ExceptionWrapper.php along with the various Form error related handlers in the SerializerBundle (and the underlying lib). you will have to dig into there to see how you could provide an alternative structure

Thanks Lukas.
I saw it was possible to override the exception_wrapper_handler. I created mine. But I didn't find a good way to pass my custom error code to the wrapper handler.
Instead I did this:

// in my controller
if (!$product) {
    throw $this->createNotFoundException([Errors::RESOURCE_NOT_FOUND, 'Product not found.']);
}
    // in my parent controller
    public function createNotFoundException($message = 'Not Found', \Exception $previous = null)
    {
        if (is_array($message)) {
            $message = json_encode($message);
        }

        return parent::createNotFoundException($message, $previous);
    }
    // in my custom ExceptionWrapperHandler
    public function wrap($data)
    {
        $decodedExceptionMessage = json_decode($data['message']);

        if (is_array($decodedExceptionMessage) && array_key_exists(0, $decodedExceptionMessage)) {
            $data['status_code'] = $decodedExceptionMessage[0];
        }

        if (is_array($decodedExceptionMessage) && array_key_exists(1, $decodedExceptionMessage)) {
            $data['message'] = $decodedExceptionMessage[1];
        }

        return new ExceptionWrapper($data);
    }
//Result
{
    "code": "resource_not_found",
    "message": "Product not found.",
    "errors": null
}

I can do the same for the form errors with the validator constraints by overriding the https://github.com/schmittjoh/serializer/blob/master/src/JMS/Serializer/Handler/FormErrorHandler.php#L119.

But I'm not happy with my solution as I have to json_encode my messages in order to show a custom info like custom error codes... It looks quite ugly.

I was hoping you could advise me on a better solution.

For mobile application form errors is a big trouble, because have a different formats (hash or array) :(

maybe someone on the rest ml can help https://groups.google.com/forum/#!forum/resting-with-symfony

Can you write whole example for change form validation format with exception_wrapper_handler ? (need for rest api)

{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "children": {
      "username": {
        "errors": [
          "This value should not be blank."
        ]
      }
    }
  }
}

to something like this

{
  "error": {
      "username": [
        "This value should not be blank."
      ]
    }
}

@ruscon there is several questions for this format:

  • how are you rendering errors attached at the root of the form rather than on a child ?
  • how does it work for deeper forms, which have child forms nesting other simpler forms ?

Errors may be attached at any level of the tree. The JMSSerializer serialization format handles it by keeping a tree in the error serialization as well. Given that you want o use a flat structure, you need to define how the flattening will happen.

@ruscon as @stof said I think the default error structure is the right one because of these 2 points. If you really want to change it, you can override this class https://github.com/schmittjoh/serializer/blob/master/src/JMS/Serializer/Handler/FormErrorHandler.php#L128 that is responsible for serializing form errors.

@stof Do you have any idea on how I can pass extra info (like custom error codes) to the exception wrapper or to the jms form error serializer?

@lucascourot no, I don't know. I never tried to customize the serialization of errors to add such error code, so I don't have a solution for you

@stof

how are you rendering errors attached at the root of the form rather than on a child ?

Errors needed only for form validation only
Don't needed code field, because it is rest api (i use http status)
Don't needed message, because i have api doc for http statuses (but may be i add this field later)
And root errors it's a trouble for me, because of this https://github.com/sonata-project/SonataUserBundle/issues/462 :(

how does it work for deeper forms, which have child forms nesting other simpler forms ?

Easily

{
  "error": {
    "user": { # user child form
      "firstname": [
        "This value should not be blank."
      ]
    },
    "agent_card": [
      "This value is not valid."
    ]
  }
}

_Why i can't use default error structure ?_

Because ios programmer can't parse different error structure (sometime hash, sometime array)

Example

{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "children": {
      "username": { # hash, if has errors
        "errors": [
          "This value should not be blank."
        ]
      }
    }
  }
}
{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "children": {
      "username": [] # array, if no errors
    }
  }
}

If you could help me with good format for rest api (considering ios strong format) - I will be very grateful!


Now i use this, but i thing should be a more correct approach
And sometimes i have errors like this https://github.com/sonata-project/SonataUserBundle/issues/462

```

namespace XBundleApiBundleEventListener;

use XBundleApiBundleExceptionInvalidFormException;
use SymfonyComponentFormForm;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelEventGetResponseForExceptionEvent;
use SymfonyComponentHttpKernelExceptionAccessDeniedHttpException;
use SymfonyComponentHttpKernelExceptionBadRequestHttpException;
use SymfonyComponentHttpKernelExceptionConflictHttpException;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;

class ExceptionFilterResponse
{
/**
* @param GetResponseForExceptionEvent $event
*/
public function onKernelResponse(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$routeName = $request->get('_route');

    // if not api route, when don't catch
    if ('api_' !== substr($routeName, 0, 4)) {
        return;
    }

    $exception =  $event->getException();

    $statusCode = 500;
    $message = $exception->getMessage();

    if ($exception instanceof NotFoundHttpException) {
        $statusCode = $exception->getStatusCode();
        $message = $exception->getMessage();

    } elseif ($exception instanceof BadRequestHttpException) {
        $statusCode = $exception->getStatusCode();
        $message = $exception->getMessage();

    } elseif ($exception instanceof ConflictHttpException) {
        $statusCode = $exception->getStatusCode();
        $message = $exception->getMessage();

    } elseif ($exception instanceof AccessDeniedHttpException) {
        $statusCode = $exception->getStatusCode();
        $message = $exception->getMessage();

    } elseif ($exception instanceof InvalidFormException) { // my custom exception with form object
        $statusCode = 400;
        $message = $this->getFormErrors($exception->getForm());

    } elseif ($exception instanceof \RuntimeException) {
        $statusCode = 400;
        $message = $exception->getMessage();

    }

    $content = json_encode(['error' => $message]);
    $response = new Response($content, $statusCode, ['ACCEPT' => 'application/json']);
    $event->setResponse($response);
}

/**
 * Get custom error structure
 * @param Form $form
 * @return array
 */
public function getFormErrors(Form $form)
{
    $errors = [];

    if ($form instanceof Form) {
        foreach ($form->getErrors() as $error) {
            $errors[] = $error->getMessage();
        }

        foreach ($form->all() as $key => $child) {
            /**
             * @var $child Form
             */
            if ($err = $this->getFormErrors($child)) {
                $errors[$key] = $err;
            }
        }
    }

    return $errors;
}

}


parameters:
x_api.kernel.listener.exception_listener.class: XBundleApiBundleEventListenerExceptionFilterResponse

services:
x_api.kernel.listener.exception_listener:
class: %x_api.kernel.listener.exception_listener.class%
tags:
- {name: kernel.event_listener, event: kernel.exception, method: onKernelResponse}
```

Easily

well, this is not easy. How do you return errors attached at the user level when it also has children ?

Recursion as you can see in

public function getFormErrors(Form $form)

test results
http://i.imgur.com/KpYdMMX.png
again will be trouble with hash and array

you still only have errors attached on leafs in your example. This is not the case I described above

+1 for consistency between hash and array

@lsmith77 How can I return errors from a different kind of exception? Which is not form errors.
I can see all the exceptions being turned into a FlattenException, which means my custom exceptions are not available at ExceptionHandler/Wrapper. How can I achieve custom errors?

Thank you!

@renatomefidf I'm struggling with the same thing. I tried writing my ExceptionWrapperHandler but there is no original exception available to retrieve additional data from it. Has anyone managed to do this? I have a custom exception which carries command validation errors and I can't find any way to show them within fos rest.

@lsmith77 what do you think about using a custom exception listener with an exception normalizer (something like the symfony normalizers) which would convert the exception to an array ?

@eXtreme sadly I have no solution for our case, you may need to do custom returns in each place you need it

@renatomefidf the easiest thing to do (and the thing I did, cuz it just works) is to write a kernel.exception listener and return JsonResponse with custom format, but it is outside fosrest.

@eXtreme

I had similar issue and solved it with ExceptionWrapperHandler:

class ExceptionWrapperHandler implements ExceptionWrapperHandlerInterface
{

    /**
     * @param array $data
     *
     * @return ExceptionWrapper
     */
    public function wrap($data)
    {
        if ($data['errors'] instanceof Form) {
            $form = $data['errors'];
            $data['errors'] = [];
            foreach ($form->getErrors(true, true) as $error) {
                $path = $error->getCause()->getPropertyPath();
                $data['errors'][$path] = $error->getMessage();
            }
        }
        return new ExceptionWrapper($data);
    }

}

In my Controller I simply return Form if it's not valid:

class UserController extends FOSRestController
{

    /**
     * @Post("/register")
     *
     * @param Request $request
     * @return array
     */
    public function registerAction(Request $request)
    {
        $form = $this->container->get('fos_user.registration.form');
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->container->get('fos_user.user_manager');

        $user = $userManager->createUser();
        $form->setData($user);

        $form->handleRequest($request);

        if (!$form->isValid()) {
            return $form;
        }

        $user->setEnabled(true);
        $userManager->updateUser($user);

        return $user;
    }

}

Example result:

{
  "code": 400,
  "message": "Validation Failed",
  "errors": {
    "data.username": "Ta nazwa u\u017cytkownika jest ju\u017c zaj\u0119ta",
    "data.email": "Podany email jest zaj\u0119ty"
  }
}

@hsz thanks, I will try it.

Should be fixed by #1358

Try this class as service. It helped me. https://gist.github.com/Graceas/6505663
Symfony Form Error Serializer. Allows tree and flat array styles for errors.

Was this page helpful?
0 / 5 - 0 ratings