Validation

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

There are a bunch of different ways that an API client can send bad data: they might send malformed JSON... or send a blank title field... maybe because a user forgot to fill in a field on the frontend. The job of our API is to respond to all of these situations in an informative and consistent way so that errors can be easily understood, parsed and communicated back to humans.

Invalid JSON Handling

This is one of the areas where API Platform really excels. Let's do some experimenting: what happens if we accidentally send some invalid JSON? Remove the last curly brace.

Try it! Woh! Nice! This comes back with a new "type" of resource: a hydra:error. If an API client understands Hydra, they'll instantly know that this response contains error details. And even if someone has never heard of Hydra before, this is a super clear response. And, most importantly, every error has the same structure.

The status code is also 400 - which means the client made an error in the request - and hydra:description says "Syntax error". Without doing anything, API Platform is already handling this case. Oh, and the trace, while maybe useful right now during development, will not show up in the production environment.

Field Validation

What happens if we just delete everything and send an empty request? Oh... that's still technically invalid JSON. Try just {}.

Ah... this time we get a 500 error: the database is exploding because some of the columns cannot be null. Oh, and like I mentioned earlier, if you're using Symfony 4.3, you might already see a validation error instead of a database error because of a new feature where validation rules are automatically added by reading the Doctrine database rules.

But, whether you're seeing a 500 error, or Symfony is at least adding some basic validation for you, the input data that's allowed is something we want to control: I want to decide the exact rules for each field.

Tip

Actually, the auto-validation was not enabled by default in Symfony 4.3, but may be in Symfony 4.4.

Adding validation rules is... oh, so nice. And, unless you're new to Symfony, this will look delightfully boring. Above title, to make it required, add @Assert\NotBlank(). Let's also add @Assert\Length() here with, how about, min=2 and max=50. Heck, let's even set the maxMessage to

Describe your cheese in 50 chars or less

... lines 1 - 14
use Symfony\Component\Validator\Constraints as Assert;
... lines 16 - 37
class CheeseListing
{
... lines 40 - 46
/**
... lines 48 - 49
* @Assert\NotBlank()
* @Assert\Length(
* min=2,
* max=50,
* maxMessage="Describe your cheese in 50 chars or less"
* )
*/
private $title;
... lines 58 - 175
}

What else? Above description, add @Assert\NotBlank. And for price, @Assert\NotBlank(). You could also add a GreaterThan constraint to make sure this is above zero.

... lines 1 - 37
class CheeseListing
{
... lines 40 - 58
/**
... lines 60 - 61
* @Assert\NotBlank()
*/
private $description;
... line 65
/**
... lines 67 - 70
* @Assert\NotBlank()
*/
private $price;
... lines 74 - 175
}

Ok, switch back over and try sending no data again. Woh! It's awesome! The @type is ConstraintViolationList! That's one of the types that was described by our JSON-LD documentation!

Go to /api/docs.jsonld. Down under supportedClasses, there's EntryPoint and here is ConstraintViolation and ConstraintViolationList, which describes what each of these types look like.

And the data on the response is really useful: a violations array where each error has a propertyPath - so we know what field that error is coming from - and message. So... it all just... works!

And if you try passing a title that's longer than 50 characters... and execute, there's our custom message.

Validation for Passing Invalid Types

Perfect! We're done! But wait... aren't we missing a bit of validation on the price field? We have @NotBlank... but what's preventing us from sending text for this field? Anything?

Let's try it! Set the price to apple, and execute.

Ha! It fails with a 400 status code! That's awesome! It says:

The type of the price attribute must be int, string given

If you look closely, it's failing during the deserialization process. It's not technically a validation error - it's a serialization error. But to the API client, it looks just about the same, except that this returns an Error type instead of a ConstraintViolationList... which probably makes sense: if some JavaScript is making this request, that JavaScript should probably have some built-in validation rules to prevent the user from ever adding text to the price field.

The point is: API Platform, well, really, the serializer, knows the types of your fields and will make sure that nothing insane gets passed. It knows that price is an integer from two sources actually: the Doctrine @ORM\Column metadata on the field and the argument type-hint on setPrice().

The only thing we really need to worry about is adding "business rules" validation: adding the @Assert validation constraints to say that this field is required, that field has a min length, etc. Basically, validation in API Platform works exactly like validation in every Symfony app. And API Platform takes care of the boring work of mapping serialization and validation failures to a 400 status code and descriptive, consistent error responses.

Next, let's create a second API Resource! A User! Because things will get really interesting when we start creating relations between resources.

Leave a comment!

  • 2020-08-03 weaverryan

    Hey Sasa Milivojevic !

    If I remember correctly, this type of error doesn't come from the @Assert\ annotations - it comes from the type-hints that you have on your setter function. My guess is that your setRegion function has a string type-hint on the argument (and maybe you also have declare(strict_types=1) on your class... I can't remember if that's needed).

    The point is: this error is because if your type-hint - API Platform is smart enough to read this and not allow an integer to be set on this field :).

    Ideally, you would not get any of these types of errors (and instead would only get true validation errors) because these types of errors are impossible to "map" correctly to the field. But this is tricky due to how Symfony's validation system works: data *first* gets set onto your object and *then* it's validated. So, you could add a @Assert\Type("string") to your property, but you would need to remove the string type-hint from your argument... which (I admit) is kind of a bummer. Removing the declare(strict_types=1) might also work (I can't remember), but again - if you like using strict types, that's a serious trade-off.

    So, sorry I can't offer an exact solution - but hopefully this helps!

    Cheers!

  • 2020-07-29 Victor Bocharsky

    Hey Sasa,

    Ah, now I see it was a question, sorry :)

    Cheers!

  • 2020-07-29 Sasa Milivojevic

    Uh sorry maybe I wasn't clear enough, I edit question

  • 2020-07-29 Victor Bocharsky

    Hey Sasa,

    Thank you for sharing this solution with others!

    Cheers!

  • 2020-07-29 Sasa Milivojevic

    One more thing how to handle this kind of errors?

    I got this error that is not in violations array

    @context: "/api/contexts/Error"
    @type: "hydra:Error"
    hydra:description: "The type of the "region" attribute must be "string", "integer" given."
    hydra:title: "An error occurred"

    and this is my asserts in entity

    /**
    * @Assert\NotNull(message="Region ne sme da bude prazno polje")
    * @Assert\NotBlank(message="Region ne moze ostati prazno polje")
    * @Assert\Length(max="50", maxMessage="Licenca moze imate maksimum {{value}} karaktera")
    * @ORM\Column(type="string", length=50, nullable=true)
    * @Groups({"region:read", "region:write", "aplication:read"})
    */
    private $region;

  • 2020-07-24 Sasa Milivojevic

    Thank you so much @weaverryan !
    this part of code is what I looking for

    const normalizedViolations = {};
    response.data.violations.forEach((violation) => {
    normalizedViolations[violation.propertyPath] = violation.message;
    });

  • 2020-07-24 weaverryan

    Hey Sasa Milivojevic !

    Yea, there are a lot of ways to handle showing validation errors in Vue... because it's "sort of simple", but can involve a lot of repetition.

    > Should I create empty ErrorProduct for field Product

    I'm not sure what you mean here - are these Vue components?

    The easiest way to accomplish this might be to have a violations data on Vue that you *set* after the AJAX call fails. Each field would conditionally render the violation if it's there (you could of course, avoid some repetition by creating a re-usable component). I think the problem that you're talking about is the fact that the violations are an array ( [ ] ) instead of an object where each field name is a *key* in that object. That, indeed, makes it harder to handle in JavaScript (because you need to loop over the whole set to look for a specific field error).

    Instead of changing this in Vue, I would use a helper function in JS to normalize however you want. For example:


    const normalizedViolations = {};
    response.data.violations.forEach((violation) => {
    normalizedViolations[violation.propertyPath] = violation.message;
    });

    Then you should be good. You could put this in a module so that you could re-use it:


    // normalize-violations.js
    export default function(violations) {
    // .. all the code above

    return normalizedViolations;
    }

    API Platform us following a "spec" with their response... and I think it's just easier to do the normalizing in JS than try to hijack API Platform.

    Let me know if this helps :).

    Cheers!

  • 2020-07-23 Sasa Milivojevic

    On front side I am working with Vue js. So if we get array violations, what is best practice to write error mesage under every input field on front.

    Should I create empty ErrorProduct for field Product,

    then for loop over violatins array and based on propertyPath put message for that Product filed in ErrorProduct,

    and then show error messages from ErrorProduct under the Product field in form?

    It's quite complicated and has code that repeats itself, so I wonder if it's possible to get errors directly from the api platform so that the errors are separate for each field or something like that

  • 2020-03-19 weaverryan

    Hey Jason Olson!

    Hmm. The way it's parsed is determined by the Doctrine annotations parser, and actually, *both* of these syntaxes should be valid (I usually *always* include the (), but I believe their optional). What error do you get when you try @Assert\NotBlank()?

    Cheers!

  • 2020-03-18 Jason Olson

    It appears that at as of Symfony 5 the @Assert\NotBlank does not require the parens for parameters, otherwise it will throw an error, therefore it is
    * @Assert\NotBlank
    and NOT
    * @Assert\NotBlank()

  • 2019-07-04 weaverryan

    Hey Pedro Carlos Abreu and @Diego

    Actually, this changed since the recording - and we'll need to add a note :). The feature does exist in Symfony 4.3, and can be enabled with some config. For about the first 2 weeks of June, if you started a new project, that configuration was present (via the recipe) and you *did* get auto validation. I was planning ahead for this. However, due to a few inflexibilities with the feature (which should be fixed for 4.4), we've disabled the feature in the browser. You can still enable it, but if you do nothing (as most people will), you will not get it.

    We'll add a note to clarify that spot.

    Cheers!

  • 2019-07-02 Diego Aguiar

    Hey @Chris

    On production you still will see the error message but it's just the stack trace part that won't be shown (You don't really want your users to see that information)

    Cheers!

  • 2019-07-02 Chris

    Hi,

    When you say "Oh, and the trace, while maybe useful right now during development, will not show up in the production environment." are you saying we can't send error information to the client? I have a React client that is waiting for the API errors to display. How can I achieve that?

    Also, thank you for the great videos!

  • 2019-07-01 Diego Aguiar

    Hey Pedro Carlos Abreu

    I'm sorry if that statement caused you some confusion. The automatic validation feature used to be enabled by default but Symfony people decided to change it. I believe it's enabled by default *only* on new projects

    Cheers!

  • 2019-06-30 Pedro Carlos Abreu

    You would be more clear with "if you're using Symfony 4.3, you MIGHT already see a validation error instead of a database error", it could be confusing because it's not enabled by default.

  • 2019-06-14 Victor Bocharsky

    Hey John,

    Ah, we're sorry about that! Yes, namespace was missed in the first code block, it's fixed now: https://symfonycasts.com/sc...

    Thank you for reporting it!

    Cheers!

  • 2019-06-13 John Christensen

    I had to add the following use statement to CheeseListing.php to get @Assert annotations to work:

    use Symfony\Component\Validator\Constraints as Assert; // to use Symfony's built-in constraints

    Was that left out or did I miss something?