Serializing entities for REST using HAL JSON
Drupal 8 has a nice REST API. In order for entities to be send as payload in a REST request they need to be serialized to a string in such a way that they can be reconstructed back into PHP objects. For this Drupal uses the Serialization API. The serialization consists of a normalization step and an encoding step, in a chosen format.
Drupal 8 core supports the formats XML, JSON and HAL JSON. Contrib modules such as JSON API enable more formats. Each format has varying support for serializing Drupal entities: some support translations, some support revisions, some support related entities. You will need to find out which format will fit your needs.
For this article I will focus on the HAL JSON format. It supports translations and related entities, and via the contrib Entity Reference Revision module also revisions. This is necessary if you want to send Nodes with Paragraphs over REST.
The base Normalizers can be found in core/modules/serialization/src/Normalizer.
In the core HAL module you can find HAL normalizers for many classes. Have a look at /core/modules/hal/src/Normalizer/. Each normalizer is called based on four parameters, two are normalizer class properties, 1 is a service tag, 1 is a function:
- supportedInterfaceOrClass: This determines for which class this normalizer will work.
- format: for HAL this defaults to "hal_json". This determines for which serialization format this normalizer will work.
- priority service tag: In /core/modules/hal/hal.services.yml you can see how each normalizer is declared as a service with a certain priority. The serializer collects all normalizers that support a certain class + format, and then only uses the highest priority normalizer. This way you can override a core nomalizer if you need some other functionality. You can restrict your override by setting and checking the $context parameter in many normalizer functions.
- depending on the process: supportsNormalization() or supportsDenormalization() is checked to see if the normalizer applies.
Serialize a node using HAL
The normalization process is a finegrained multi-level process. If you serialize a node using the HAL JSON format, the following normalization happens:
you call serialize($node) on vendor/symfony/serializer/Serializer
- the serializer class finds the applicable normalizer and calls normalize() on this normalizer
- for HAL the ContentEntityNormalizer::normalize() is called
- iterates over each entity field, and calls a normalizer for each field.
- for HAL the FieldNormalizer::normalize() is called
- iterates over each translation of the field
- iterates over each field item and calls a normalizer for each field item
- for HAL the FieldItemNormalizer::normalize() is called
- calls a normalizer for the field value
- the core PrimitiveDataNormalizer::normalize() is called
- calls the getCastedValue() function on the TypedData object. These live in core/lib/Drupal/Core/TypedData/Plugin/DataType. For strings this results in getString().
Using this process the node is serialized. For more complex cases, different HAL normalizers are provided. For file entities there is the FileEntityNormalizer which overrides ContentEntityNormalizer. For entity reference fields there is the EntityReferenceItemNormalizer which overrides the FieldItemNormalizer. The EntityReferenceItemNormalizer calls normalize on the target entity, which invokes the HAL ContentEntityNormalizer again. But this time it requests only the uuid field, so all others are skipped. The uuid is then supplied in a special _embedded array key.
Related entities are supplied as a REST link path and a UUID. This way during denormalization you can check if the entity already exists in your system and rebuild the relationship using the UUID, and otherwise retrieve the related entity using the supplied REST path.
The contrib Entity Reference Revision module provides a EntityReferenceRevisionItemNormalizer which overrides the EntityReferenceItemNormalizer. This normalizer adds the target_revision_id field to the _embedded array (NB this is a local ID!), or in the case of entities which have entity_revision_parent_id_field set (such as Paragraph entities) the full referenced entity is normalized and included.
Once a node is normalized it gets serialized using the requested encoding. You can then send the node in a REST request.
Deserialize a node using HAL
When you define a REST endpoint and resource, the incoming data that is supplied to your rest resource is already decoded and denormalized for you by the Serialization service.
If you deserialize a node using the HAL JSON format, the following denormalization happens:
- the serializer class finds the applicable denormalizer and calls denormalize() on this denormalizer
- The class to which the serialized data will be denormalized is determined by your REST endpoint annotation serialization_class.
- for HAL the ContentEntityNormalizer::denormalize() is called
- ignores the requested class to denormalize to, and determines the class based on the link path set in the data array using \Drupal\hal\LinkManager\TypeLinkManager::getTypeInternalIds().
- flattens _embedded array into reference fields
- constructs a new empty entity of the required type.
- iterates over each entity field (not the supplied fields but the fields in the entity definition), and sets the set default values to  and calls a denormalizer for each field.
- FieldNormalizer::denormalize() is called (NB this is not a HAL specific normalizer, but the generic Serialization version)
- iterates over each field item and calls a denormalizer for each field item
- creates empty field items on the entity using appendItem()
- for HAL the FieldItemNormalizer::normalize() is called
- create empty translated field items using appendItem()
- sets the field value using constructValue(), which defaults to the supplied data in the serialized array.
Using this process the node is deserialized. For more complex cases, different HAL denormalizers are provided. For file entities there is the FileEntityNormalizer which overrides ContentEntityNormalizer. For entity reference fields there is the EntityReferenceItemNormalizer which overrides the FieldItemNormalizer. The EntityReferenceItemNormalizer only overrides the constructValue() function during denormalization. It uses the EntityResolver service to map the supplied UUID to a local entity ID and thus restore the entity reference value target_id.
The contrib Entity Reference Revision module provides a EntityReferenceRevisionItemNormalizer which overrides the EntityReferenceItemNormalizer. This denormalizer adds the target_revision_id (NB remote ID!) to the field value, or in the case of entities which have entity_revision_parent_id_field set (such as Paragraph entities) the full referenced entity is normalized and included.
About the author
Finne Fortuin is a Drupal expert who speaks UK english but uses the US spelling for nomalizers because this keeps it in line with the US code spelling of the classes and functions in Drupal.