If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSi la única forma de controlar la entrada y la salida de nuestra API fuera controlar los getters y setters de nuestra entidad, no sería tan flexible... y podría ser un poco peligroso. ¡Podrías añadir un nuevo método getter o setter para algo interno y no darte cuenta de que estabas exponiendo nuevos datos en tu API!
La solución para esto -y la forma en que recomiendo hacer las cosas en todos los casos- es utilizar grupos de serialización.
En la anotación, añade normalizationContext
. Recuerda que la normalización se produce cuando pasas de tu objeto a un array. Así que esta opción está relacionada con el momento en que estás leyendo datos de tu API. El contexto es básicamente "opciones" que pasas a ese proceso. La opción más común, con diferencia, se llama "groups"
, que se establece en otro array. Añade una cadena aquí: cheese_listing:read
.
... lines 1 - 8 | |
/** | |
* @ApiResource( | |
... lines 11 - 16 | |
* normalizationContext={"groups"={"cheese_listing:read"}} | |
* ) | |
... line 19 | |
*/ | |
class CheeseListing | |
... lines 22 - 125 |
Gracias a esto, cuando se serialice un objeto, el serializador sólo incluirá los campos que estén en este grupo cheese_listing:read
, porque, en un segundo, vamos a empezar a añadir grupos a cada propiedad.
Pero ahora mismo, no hemos añadido ningún grupo a nada. Y así, si vas e intentas tu operación de colección get... ¡oh! ¡Ah! ¡Un gran error!
Vamos a... hacer como si lo hubiera hecho a propósito y ver cómo depurarlo! El problema es que el gigantesco error HTML es... un poco difícil de leer. Una forma de ver el error es utilizar nuestro truco de antes: ir a https://localhost:8000/_profiler/
.
¡Woh! Vale, hay dos tipos de errores: los errores de ejecución, en los que algo ha ido mal específicamente en esa petición, y los errores de compilación, en los que alguna configuración no válida está matando todas las páginas. La mayoría de las veces, si ves una excepción, todavía hay un perfilador que puedes encontrar para esa petición utilizando el truco de ir a esta URL, encontrar esa petición en la lista - normalmente justo en la parte superior - y hacer clic en el sha en su perfilador. Una vez allí, puedes hacer clic en la pestaña "Excepción" de la izquierda para ver la gran y hermosa excepción normal.
Si tienes un error de compilación que mata todas las páginas, es aún más fácil: lo verás cuando intentes acceder a cualquier cosa.
De todos modos, el problema aquí es con mi sintaxis de anotación. Lo hago a menudo, lo cual no es un gran problema siempre que sepas cómo depurar el error. Y, ¡sí! He olvidado una coma al final.
¡Actualiza de nuevo! El perfilador funciona, así que ahora podemos volver a darle a ejecutar. Compruébalo: tenemos @id
y @type
de JSON-LD... ¡pero no contiene ningún campo real porque ninguno está en el nuevo grupo cheese_listing:read
!
Copia el nombre del grupo cheese_listing:read
. Para añadir campos a éste, por encima del título, utiliza @Groups()
, {""}
y pégalo. Pongamos también eso por encima de description
... y price
.
... lines 1 - 7 | |
use Symfony\Component\Serializer\Annotation\Groups; | |
... lines 9 - 21 | |
class CheeseListing | |
{ | |
... lines 24 - 30 | |
/** | |
... line 32 | |
* @Groups({"cheese_listing:read"}) | |
*/ | |
private $title; | |
... line 36 | |
/** | |
... line 38 | |
* @Groups({"cheese_listing:read"}) | |
*/ | |
private $description; | |
... line 42 | |
/** | |
... lines 44 - 46 | |
* @Groups({"cheese_listing:read"}) | |
*/ | |
private $price; | |
... lines 50 - 127 | |
} |
Dale la vuelta y vuelve a intentarlo. ¡Muy bien! Obtenemos esos tres campos exactos. Me encanta este control.
Por cierto, el nombre cheese_listing:read
... Me lo acabo de inventar - puedes usar cualquier cosa. Pero, voy a seguir una convención de nomenclatura de grupos que recomiendo. Te dará flexibilidad, pero mantendrá las cosas organizadas.
Ahora podemos hacer lo mismo con los datos de entrada. Copia normalizationContext
, pégalo, y añade de
delante para hacer denormalizationContext
. Esta vez, utiliza el grupo cheese_listing:write
... lines 1 - 9 | |
/** | |
* @ApiResource( | |
... lines 12 - 18 | |
* denormalizationContext={"groups"={"cheese_listing:write"}} | |
* ) | |
... line 21 | |
*/ | |
... lines 23 - 130 |
Copia esto y... veamos... sólo añade esto a title
y price
por ahora. En realidad no queremos añadirlo a description
. En su lugar, hablaremos de cómo añadir este grupo al falso textDescription
en un minuto.
... lines 1 - 22 | |
class CheeseListing | |
{ | |
... lines 25 - 31 | |
/** | |
... line 33 | |
* @Groups({"cheese_listing:read", "cheese_listing:write"}) | |
*/ | |
private $title; | |
... lines 37 - 43 | |
/** | |
... lines 45 - 47 | |
* @Groups({"cheese_listing:read", "cheese_listing:write"}) | |
*/ | |
private $price; | |
... lines 51 - 128 | |
} |
Muévete y actualiza de nuevo. ¡Abre la ruta POST.... y ahora los únicos campos que podemos pasar son title
y price
!
Así que normalizationContext
y denormalizationContext
son dos configuraciones totalmente separadas para las dos direcciones: lectura de nuestros datos - normalización - y escritura de nuestros datos - desnormalización.
En la parte inferior de los documentos, también te darás cuenta de que ahora tenemos dos modelos: el modelo de lectura - que es el contexto de normalización con title
, description
yprice
, y el modelo de escritura con title
y price
.
Y, no es realmente importante, pero puedes controlar estos nombres si quieres. Añade otra opción: swagger_definition_name
ajustada a "Lectura". Y a continuación lo mismo... ajustado a Escritura.
... lines 1 - 9 | |
/** | |
* @ApiResource( | |
... lines 12 - 17 | |
* normalizationContext={"groups"={"cheese_listing:read"}, "swagger_definition_name"="Read"}, | |
* denormalizationContext={"groups"={"cheese_listing:write"}, "swagger_definition_name"="Write"} | |
* ) | |
... line 21 | |
*/ | |
... lines 23 - 130 |
Normalmente no me importa esto, pero si quieres controlarlo, puedes hacerlo.
Pero, ¡nos faltan algunos campos! Cuando leemos los datos, obtenemos title
,description
y price
. ¿Pero qué pasa con nuestro campo createdAt
o nuestro campo personalizado createdAtAgo
?
Imaginemos que sólo queremos exponer createdAtAgo
. ¡No hay problema! Sólo tienes que añadir la anotación @Groups
a esa propiedad... oh, espera... no hay ninguna propiedad createdAtAgo
. Ah, es igual de fácil: busca el getter y pon la anotación allí:@Groups({"cheese_listing:read"})
. Y ya que estamos aquí, añadiré algo de documentación a ese método:
Hace cuánto tiempo en texto que se añadió este listado de quesos.
... lines 1 - 22 | |
class CheeseListing | |
{ | |
... lines 25 - 112 | |
/** | |
* How long ago in text that this cheese listing was added. | |
* | |
* @Groups("cheese_listing:read") | |
*/ | |
public function getCreatedAtAgo(): string | |
... lines 119 - 133 | |
} |
¡Vamos a probarlo! Actualiza la documentación. Abajo, en la sección de modelos... ¡qué bien! Ahí está nuestro nuevo campo createdAtAgo
de sólo lectura. Y la documentación que hemos añadido aparece aquí. ¡Muy bien! No es de extrañar que cuando lo probamos... el campo aparezca.
Para la desnormalización -para el envío de datos- tenemos que volver a añadir nuestro campo falso textDescription
. Busca el método setTextDescription()
. Para evitar que los clientes de la API nos envíen directamente el campo description
, eliminamos el método setDescription()
. Por encima de setTextDescription()
, añadimos @Groups({"cheese_listing:write"})
. Y de nuevo, vamos a darle a esto algunos documentos adicionales.
... lines 1 - 88 | |
/** | |
* The description of the cheese as raw text. | |
* | |
* @Groups("cheese_listing:write") | |
*/ | |
public function setTextDescription(string $description): self | |
... lines 95 - 140 |
Esta vez, cuando refresquemos los documentos, podrás verlo en el modelo de escritura y, por supuesto, en los datos que podemos enviar a la operación POST.
Y... ¡esto nos lleva a una gran noticia! Si decidimos que algo interno de nuestra aplicación necesita establecer la propiedad de descripción directamente, ahora es perfectamente posible volver a añadir el método original setDescription()
. Eso no formará parte de nuestra API.
... lines 1 - 88 | |
public function setDescription(string $description): self | |
{ | |
$this->description = $description; | |
return $this; | |
} | |
... lines 95 - 147 |
Vamos a probar todo esto. Actualiza la página de documentos. Creemos un nuevo listado: Delicioso chèvre -disculpa mi francés- por 25 dólares y una descripción con algunos saltos de línea. ¡Ejecuta!
¡Woh! ¡Un error 500! Podría ir a mirar esta excepción en el perfilador, pero ésta es bastante fácil de leer: una excepción en nuestra consulta: is_published
no puede ser nulo. Oh, eso tiene sentido: el usuario no está enviando is_published
... así que nadie lo está estableciendo. Y está establecido como no nulo en la base de datos. No te preocupes: pon la propiedad por defecto en false
.
... lines 1 - 22 | |
class CheeseListing | |
{ | |
... lines 25 - 59 | |
private $isPublished = false; | |
... lines 61 - 145 | |
} |
Tip
En realidad, la autovalidación no estaba activada por defecto en Symfony 4.3, pero puede que lo esté en Symfony 4.4.
Por cierto, si estás usando Symfony 4.3, en lugar de un error de Doctrine, puede que hayas obtenido un error de validación. Esto se debe a una nueva función en la que las restricciones de la base de datos Doctrine pueden utilizarse automáticamente para añadir validación. Así que, si ves un error de validación, ¡genial!
De todos modos, intenta ejecutarlo de nuevo. ¡Funciona! Tenemos exactamente los campos de entrada y salida que queremos. El campo isPublished
no está expuesto en absoluto en nuestra API, pero se está configurando en segundo plano.
A continuación, vamos a aprender algunos trucos más de serialización, como el control del nombre del campo y el manejo de los argumentos del constructor.
Hey John,
You're totally right, we forgot to show the namespace in the code block, I fixed it in https://github.com/SymfonyC... - the correct code block will be available very soon on the website.
Thank you for reporting it!
Cheers!
I had troubles with serialization groups. For thouse with similar problem, maybe my solution will help.
Try to add
`serializer:
enable_annotations: true`
to framework settings (config/packages/framework.yaml).
(I'm on symfony 6.0.3 and PHP 8)
Hey Krzysztof R.
Can you please provide more info about issue you got?
I guess it's related to PHP Attributes, some packages uses attributes instead of annotation, when supported PHP version is installed
Cheers!
Yes I'm using attributes, but annotations also did't work for me.
It was working fine for regular entity fields. But when I try to add e.g. textDescription field to the API it was not present in schema or endpoint (I tried to send it anyway, but didn't work).
When I enabled annotations in serializer config it started to work ;)
interesting Attributes should work without additional configuration IIRC...
Thanks again for investigation
I was starting from this boilerplate
https://github.com/martinso...
maybe there was something wrong with config. I'm not that much into symfony to check that. Anyway it is working perfect now so all good ;)
Cheers ;)
Hi,
I'm having a problem with Serialization groups and Inhetitance.
Let you have two related entities: Blog and Comment. Both inerit from an abstract base "Content" class.
The column "body" is on the Content Class. As usual a Blog can have one or more Comment.
The problem is that I cannot figure out how to handle the body; In the Blog detail endpoint, I want to show the list of comment with some details ( ex: the title of the comment). Cause the body is a property shared between Blog and Comment, and I want to show the body on Blog endpoint, the body will be visible also con each comment of the blog.
I just want to see the body of the blog, not for all related comments.
I tried with a common "content:item:read", but this group show the body of the comment also in blog entrypoint.
Which is the best way to handle groups with Inheritance?
Hey Gianluca-F!
Yea, this is tricky because, thanks to the inheritance, you can't control make the serialization groups (at when using attributes) different on the body
field for Blog
vs Comment
. Using simple methods, I can't think of a way to do this: no matter what serialization group you add to body
, if that group is returned on the Blog detail endpoint, then it will definitely also be returned in the embedded comments from that endpoint. So, unless I'm missing some nice trick, I also don't see an easy solution!
What options do we have? I can think of a few, none of which are super great:
A) You could use XML for your serialization configuration. This would allow you to put different serialization groups for body
on each class. The problem, of course, is that using attributes is way nicer. I'm not sure if you can mix XML and attributes (like use attributes for everything, but then just XML for the body
field). My guess is that you can't.
B) Stop using inheritance. I'm sure you have some good reasons to use inheritance, but if you stopped, the problem goes away. If there is some logic you want to share, you could move the methods it into a trait (but not the property itself). Then, use the trait in Blog
and Comment
but then manually add the property to each class individually.
Beyond that, you could probably find some more technical trick... like maybe decorating the normalizer and then, if you detect that you are able to normalize a Comment
in the context of a Blog
, you change the normalization groups in $context
before calling the inner normalizer. Probably possible, but pretty technical.
Let me know what you end up doing!
Cheers!
Hi Ryan,
thanks for your reply; yes as I go ahead with development, I'm having some issues with inheritance.
The reason why I choose the inheritance is that all these entities share up to 90% of properties ( a blog or a commet IS a generic content ) , so inheritance is natural BUT ... with API I'm having problems.
Another problem is with ApiIdentifier.
For news, I have that ID is the ApiIdentifier ( id is not shared ) but for another entity, SLUG is the ApiIdentifier ( slug is shared in base class ).
Also in this case, I have to copy the property also on Child entity, to have only one prop as ApiIdentifier.
I could continue to use BaseClass for setter and getter, instead of dozen of traits ... not very clear but ... who really care ?? :-)
Hi,
Is there a way to set the serialization group dynamically, using, for example, a parameter on query string?
Hello GianlucaF
If I understood you correctly, then you can do it with context builder. You will find it in the security part of ApiPlatform series
PS fast link https://symfonycasts.com/sc...
StayTuned! Cheers!
I am having a strange issue whereby my setTextDescription is not coming through on my swagger page for the post action
Hey, Diego!
I have the same problem as Sam. My setTextDescription just won't show up for the POST action when I use this serialization groups. I tried to change the name to setDescription and then it shows.
Hey @Alexsander!
Thanks for the extra info! This is interesting. I'm wondering 2 things:
1) If you use setTextDescription() (where it son't show up for the POST action), can you actually *send* a textDescription field in the JSON and have it update?
2) If you use setTextDescription() with an @SerializedName("description") above the method, does it show up then?
This is odd enough that I'm wondering if something changed in newer versions of ApiPlatform. If they did, my guess is that would be unintentional (i.e. a bug).
Cheers!
Hey Ryan!
So I tried to do all over again and the problem is still the same.
1) When I (as shown in the tutorials) delete setDescription() and use setTextDescription() method instead everything is working fine. I have textDescription field and I can send/update database with this field. But as soon as I use serialization groups this field is gone from POST operation. I have only "title" and "price" fields. I also want to point out that in my "write model/schema" the "textDescription" field is shown, but in POST operation it is not shown.
2) No even in this case it won't show up. But since I can see this textDescription field in my "write model", this command updates it from "textDescription" to "description" as expected.
So when I use the post action along with @SerializedName("description") then the method works so it seems to be working as planned the only issue is it does not show up on the swagger page
Hey Sam Z. & @Aleksander!
Sorry for the slow reply - I had some time off this week :). Ok, with these new updates... and the fact that there are two of you having the issue - I'm *fairly* sure that this is some bug in API Platform in a newer version (than we use in the tutorial). I'd recommend opening an issue in API Platform if you can with the details. I can't (at the moment) test this myself... because I'm on SUCH slow internet that Composer barely works 🤦♂️
Anyways, if either if you DO create an issue, please link me to it and I can look / add any extra info to the issue :).
Cheers!
Did anyone open an issue yet? I can't find any on GitHub.
I happen to have the exact same problem as @Aleksander and @Sam Zlotnick.
Did you guys open an issue or find out how to work around that bug?
I also made the following observation:
the description field is shown in the Schemas/Model section from the swagger doc:
<blockquote>cheeses-Write{
title string
minLength: 2
maxLength: 50
price* integer
description string
writeOnly: true
The description of the cheese as raw text.
}</blockquote>
But under POST /api/cheeses Creates a cheeses resource
the field is missing, as the Example Value just shows:<br />{<br /> "title": "string",<br /> "price": 0<br />}<br />
But when i click on "Schema", just next to "Example Value" it says:<br />cheeses.jsonld-Read{<br />title string<br />minLength: 2<br />maxLength: 50<br />price* integer<br />}<br />
Am i confused, or shouldn't it say"Write" instead of read here?
Hey Tristan P.!
As I just mentioned on a different chapter, I think this issue is the same as the other one - https://symfonycasts.com/sc...
I am also not aware of any issue that's been opened yet - I would love if you could open it!
Cheers!
Thanks for the reply, I opnened an issue on friday: https://github.com/api-plat...
btw: great course and teacher!
Ah, great work Tristan P.! And it looks like someone has linked to another issue - https://github.com/api-plat... and another issue https://github.com/api-plat... which might be the same thing. That is GREAT news... because it makes it seem like the bug is definitely real and (hopefully) it should get attention. One user says that downgrading to 2.6.3 fixes it. I've added a comment to https://github.com/api-plat... to see if I can motivate someone to do some extra digging ;).
Cheers!
The below are the 2 setters I have for the description however in the swagger page under post it only shows title and price for the inputs
/**
* @ORM\Column(type="text")
* @Groups("cheese_listing:read")
*/
private $description;
public function getDescription(): ?string {
return $this->description;
}
/**
* The description of the cheese as raw text.
*
* @Groups("cheese_listing:write")
*/
public function setTextDescription(string $description): self {
$this->description = nl2br($description);
return $this;
}
Hi, I'm using api-platform configuration with yaml and not with annotations, also the serializer, so, I'm struggling to get a getProperty() to work with groups, like your example of getCreatedAtAgo(), but, in my case, I can't or I shouldn't put "Groups({"read"})" annotation on that method, I must set it in the yaml. Is this possible anyway?
PD: Sorry for my english :)
Hey Nestor B.
I think that's possible, you can configure your groups by entity like shown in this example: https://api-platform.com/do...
Cheers!
Hello. I have 2 groups to read:
* @ApiResource(
* normalizationContext={"groups"={"group:read","group_clear:read"}}
* )
By default works "group:read" context. How can I make a request to the api platform to get data in context "group_clear:read"?
Hey Dmitriy!
It sounds like you want to be able to add a group dynamically - maybe based on the currently-authenticated user or maybe a query parameter. Is that right? If so check out a custom context builder - we do something (sort of) similar here - https://symfonycasts.com/sc...
I hope that helps! Cheers!
Thank you, Ryan. I solved this problem with:
https://api-platform.com/do...
Your solution is interesting, but so far too complicated for me )
Hey Dmitriy!
Happy you got it working - my solutions are not always the only, or even the best ones ;).
Cheers!
Properties not showing up when I use normalization contexts groups. Any ideas? Code example here: https://pastebin.com/UtyHsZt5
Hey David B.
Hmm, your code looks correct. It makes me wonder if for some reason your cache didn't refresh automatically. Try deleting it manually rm var/cache/*
and try again. Also, double-check that you're running in the dev environment because if you're in prod, then you have to refresh the cache after every single change
Cheers!
Thank you for responding so quickly! I manged to figure it out shortly after I posted the question. Out of pure frustration I did a cache:clear and that resolved the issue. My DEV evn is running on a virtual machine so i'll chalk it up to a VM issue.
Hello,
Thanks for the tutorial. I am following it and trying to code along with an example project.
But as soon as I added the serialization groups I had a problem with the properties. For example one is only read-only anymore but has the write group as well. I also tried to make the request and ignore the docs but it didn't work.
I pushed the project to github: https://github.com/schnetzi...
Can you explain to me if I made any mistakes? Is the indentation in the comments important?
Thanks in advance!
I investigated a little bit further and it seems like the underscore in the property is causing the problem/error. For example `group_name` is read-only even though it has the serialization group to write but `position` works just fine. It's also with all the other underscore-properties.
When I set the properties to public the readonly is gone and it works.
Hey Paul
That's happening because you have an inconsistency in your "casing", I noticed that you have some "camel cased" properties and others in the "snake case" form. In theory, you should not do that, if you can refactor it, great! If not, you can choose which "Name Converter" to use (check this out: https://api-platform.com/do... ), or, you can use the "@SerializedName" annotation to choose a valid name https://symfony.com/doc/mas...
I hope this helps. Cheers!
Wow good catch! Thanks a lot. I changed my properties to be camel-case now and it works as `private` properties.
Hello,
Thanks for this awesome tutorial! But I did notice one small thing, in the video for this chapter, the symbols under the SymfonyCast logo and in the popup prompt are shown as question mark boxes.
Cheers!
Hey Hintay
ha that's a great catch! Thanks for the feedback, we will investigate this questions marks! )))
Cheers!
HI
Thanks for nice tutorial. I got two questions:
1. Can I add multiple read and write groups?
2. How can I differentiate between multiple read and write groups, while making API call?
TIA
Hey infete
1) Yes, you can add as many groups as you want to your API resources
2) I don't fully get your question. What you want to achieve?
Cheers!
Hello,
I think MolloKhan is talking about conditional API response.
ie "Is there a way to ask for a specific normalization/denormalization context or group while consuming the API?"
use case:
User A can GET Title and price (cheese_listing:read_limited)
User B can GET Title, price and description (cheese_listing:read)
Hey JulienG
In that case what you are looking for is "voters". In this chapter you can see a good example of how to implement them with ApiPlatform https://symfonycasts.com/sc...
I also recommend watching this chapter (and a few more after it) https://symfonycasts.com/sc...
Cheers!
I figured this out. I noticed in the video that the groups annotation can be set above the getter methods so that fixed my issue. I move all group tags to getter methods.
I have a situation with the groups annotation is not showing all of the properties that I have labeled with groups. @Groups({"pharmacies_list:read"})<br />{<br /> "@context": "string",<br /> "@id": "string",<br /> "@type": "string",<br /> "ncpdp": "string",<br /> "npi": "string",<br /> "city": "string",<br /> "state": "string",<br /> "zipcode": "string"<br />}
These are what I am getting back but business, address, and phone number is missing. How can I figure out what I wrong when there are no error message in the logs
Thought I would throw this in for good measure.
`
I noticed that the properties that were skipped have underscores in them. How do I fix this?
Hey sherwingaddis
> I noticed that the properties that were skipped have underscores in them. How do I fix this?
What you mean with that? I would need to see your code to get a better idea of what's going on (at least just that entity)
Cheers!
MolloKhan i have the same as Sherwin expierences. For example if a property in my Entity is named: "$elapsed_time" (notice the underscore) it's not showing when it's assigned to a serialization group. And for example the property $distance (without underscore) is showing up.
`
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Carbon\Carbon;
use Doctrine\ORM\Mapping as ORM;
/**
/**
* @Groups({"detailedsegmenteffort:read"})
* @ORM\Column(type="bigint", nullable=true)
*/
private $elapsed_time;
/**
* @Groups({"detailedsegmenteffort:read"})
* @ORM\Column(type="float", nullable=true)
*/
private $distance;
.....
}
`
Hey lexhartman
Seems like it's related to this problem https://github.com/api-platform/core/issues/1554
What you can do is to play with the @SerializedName()
annotation to make it match to your property getter/setter
From the docs: https://symfony.com/doc/current/components/serializer.html#using-camelized-method-names-for-underscored-attributes - just scroll down a bit
namespace App\Entity;
use Symfony\Component\Serializer\Annotation\SerializedName;
class Person
{
/**
* @SerializedName("customer_name")
*/
private $firstName;
public function __construct($firstName)
{
$this->firstName = $firstName;
}
// ...
}
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.17.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}
To get the @Groups annotation to work, I had to add this use statement:
use Symfony\Component\Serializer\Annotation\Groups;
Unless I missed something earlier, that step should be added to the <i>Adding a Group for Normalization</i> section.