Overview

Drupal 8 core provides for solid REST capabilities out-of-the-box, which is great for integrating with a web service or allowing a third-party application to consume content. However, the REST output provided by Drupal core is in a certain structure that may not necessarily satisfy the requirements as per the structure the consuming application expects.

In comes normalizers that will help us alter the REST response to our liking. For this example, we will be looking at altering the JSON response for node entities.
 

Getting Started

First, let’s install and enable the latest stable version of the “REST UI” module:

composer require drupal/restui;
drush en restui -y;

Go to the REST UI page (/admin/config/services/rest) and enable the “Content” resource:

You should see the resource enabled.

Enable “GET”, check “json” as the format, and “cookie” as the authentication provider: 
Enable Drupal core’s “rest” module. This will also enable the “Serialization” module as it is a dependency:

 

Create a test node and fill in one or all of the fields. We will be requesting and altering the structure of the core node REST resource for this node output.

After you’ve created your node, append ?_format=json to the end of your node’s URL (so it looks something like /node/1?_format=json) and access that page. You should see a JSON dump with the field names and the values for the node entity similar to:

This is great, but what if we wanted this output to be structured differently? We can normalize this output!
 

Creating the Normalizers

Create a custom module that will contain our custom normalizers. The module structure should look like:

custom/
├── example_normalizer\r\n│   ├── example_normalizer.info.yml\r\n│   ├── example_normalizer.module
│   ├── example_normalizer.services.yml
│   └── src
│       └── Normalizer
│           ├── ArticleNodeEntityNormalizer.php
│           ├── CustomTypedDataNormalizer.php
│           └── NodeEntityNormalizer.php

Each normalizer must extend NormalizerBase.php and implement NormalizerInterface.php. At the minimum, the normalize must define:

  • protected $supportedInterfaceOrClass - the interface or class that the normalizer supports.

  • public function normalize($object, $format = null, array $context = array()) {} - performs the actual “normalizing” of an object into a set of arrays/scalars.

Let’s write a normalizer to remove those nested field “value” keys:

CustomTypedDataNormalizer.php

<?php
namespace Drupal\example_normalizer\Normalizer;

use Drupal\serialization\Normalizer\NormalizerBase;

/**
 * Converts typed data objects to arrays.
 */
class CustomTypedDataNormalizer extends NormalizerBase {
  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\TypedData\TypedDataInterface';

  /**
   * {@inheritdoc}
   */
  public function normalize($object, $format = NULL, array $context = array()) {
    $value = $object->getValue();
    if (isset($value[0]) && isset($value[0]['value'])) {
      $value = $value[0]['value'];
    }
    return $value;
  }
}

 

 

We set our $supportedInterfaceOrClass protected property to Drupal\\Core\\TypedData\\TypedDataInterface (so we can make some low-level modifications with the values for the entity). This means that this normalizer supports any object that is an instance of Drupal\\Core\\TypedData\\TypedDataInterface. In the normalize() method, we check if the value contains the [0][‘value’] elements, and if so, just return the plain value stored in there. This will effectively remove the “value” keys from the output.

example_normalizer.services.yml
We need to allow Drupal to detect this normalizer, so we put it in our *services.yml and tag the service with normalizer”:

services:
 example_normalizer.typed_data:
 class: Drupal\\example_normalizer\\Normalizer\\CustomTypedDataNormalizer
 tags:
 - { name: normalizer, priority: 2 }

One important thing to notice here is the “priority” value. By default, the “serialization” module provides a normalizer for typed data:

serializer.normalizer.typed_data:
 class: Drupal\\serialization\\Normalizer\\TypedDataNormalizer
 tags:
 - { name: normalizer }

In order to have our custom normalizer get picked up first, we need to set a priority higher than the one that already exists that supports the same interface/class. When the serializer requests the normalize operation, it will process each normalizer sequentially until it finds one that applies. This is the “Chain-of-Responsibility” (COR) pattern used by Drupal 8 where each service processes the objects it supports and the rest are passed to the next processing service in the chain.

Make sure to clear the cache so the new normalizer service is detected. 

If we go to our JSON output for the node again, we can see that the output has changed a bit. We no longer see the nested “values” keys being displayed (and looks much cleaner, as well):

Great! Now, what if we want to add some custom values to our output? Let’s say we want the link to the node and an ISO 8601-formatted “changed” timestamp.

Altering node entity JSON output

We can create a normalizer that will make these modifications. Add an entry in the *services.yml file:

example_normalizer.node_entity:
 class: Drupal\\example_normalizer\\Normalizer\\NodeEntityNormalizer
  arguments: ['@entity.manager']
  tags:
    - { name: normalizer, priority: 8 }

And our normalizer:

NodeEntityNormalizer.php

<?php

namespace Drupal\example_normalizer\Normalizer;

use Drupal\serialization\Normalizer\ContentEntityNormalizer;
use Drupal\Core\Datetime\DrupalDateTime;

/**
 * Converts the Drupal entity object structures to a normalized array.
 */
class NodeEntityNormalizer extends ContentEntityNormalizer {
  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\node\NodeInterface';

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = array()) {
    $attributes = parent::normalize($entity, $format, $context);

    // Convert the 'changed' timestamp to ISO 8601 format.
    $changed_timestamp = $entity->getChangedTime();
    $changed_date = DrupalDateTime::createFromTimestamp($changed_timestamp);
    $attributes['changed_iso8601'] = $changed_date->format('c');

    // The link to the node entity.
    $attributes['link'] = $entity->toUrl()->toString();

    // Re-sort the array after our new additions.
    ksort($attributes);

    // Return the $attributes with our new values.
    return $attributes;
  }
}

Similar to before, we have to define our supported interface or class. For this normalizer, we only want to support node entities, so we set it to Drupal\\node\\NodeInterface (which means any object that implements Drupal\\node\\NodeInterface).

Our custom node normalizer extends the Drupal\\serialization\\Normalizer\\ContentEntityNormalizer class that is provided by the “serialization” module. The only thing we want to do is append 2 new values to the output to what is already provided -- the “link” and “changed_iso8601” values.

To format our timestamp, we get the timestamp from the entity object, create a DrupalDateTime object, and then format it using the PHP date format “c” character for ISO 8601. This value will be assigned to our new key on the $attributes array that holds all the values.

We will create the “link” value by using the toUrl() and toString() methods to get the URL which gets assigned to the “link” key of the $attributes array. 

After clearing cache and visiting the node output again, we do indeed see our new additions:

Altering a specific node entity type JSON output

So, we were able to alter the output for all nodes, but there may be cases where we would only want to alter the JSON output of specific node types. Fortunately, there isn’t much more to do than what we already learned. We will create a normalizer that contains a custom “changed” timestamp format that will only apply to “article” nodes.

Add another entry to our *services.yml file:

example_normalizer.article_node_entity:
 class: Drupal\\example_normalizer\\Normalizer\\ArticleNodeEntityNormalizer
  arguments: ['@entity.manager']
  tags:
    - { name: normalizer, priority: 9 }

ArticleNodeEntityNormalizer.php

<?php

namespace Drupal\example_normalizer\Normalizer;

use Drupal\serialization\Normalizer\ContentEntityNormalizer;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\node\NodeInterface;

/**
 * Converts the Drupal entity object structures to a normalized array.
 */
class ArticleNodeEntityNormalizer extends ContentEntityNormalizer {
  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\node\NodeInterface';

  /**
   * {@inheritdoc}
   */
  public function supportsNormalization($data, $format = NULL) {
    // If we aren't dealing with an object or the format is not supported return
    // now.
    if (!is_object($data) || !$this->checkFormat($format)) {
      return FALSE;
    }
    // This custom normalizer should be supported for "Article" nodes.
    if ($data instanceof NodeInterface && $data->getType() == 'article') {
      return TRUE;
    }
    // Otherwise, this normalizer does not support the $data object.
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = array()) {
    $attributes = parent::normalize($entity, $format, $context);

    // Convert the 'changed' timestamp to ISO 8601 format.
    $changed_timestamp = $entity->getChangedTime();
    $changed_date = DrupalDateTime::createFromTimestamp($changed_timestamp);
    $attributes['article_changed_format'] = $changed_date->format('m/d/Y');

    // Re-sort the array after our new addition.
    ksort($attributes);

    // Return the $attributes with our new value.
    return $attributes;
  }
}

In this normalizer, you will notice that we’re defining a new method. public function supportsNormalization($data, $format = null)  {} allows for performing more granular checks for the objects that are instances of the class/interface defined in $supportedInterfaceOrClass. In our supportsNormalization(), we check if the type of the node is an “article” using the getType() function. If so, it returns TRUE - indicating that this normalizer supports the node. Otherwise, it will return FALSE - and using the COR pattern,  it will process the next normalizer in sequence until it finds one that applies to the object.

Let’s take a look at the JSON output of a test “article” node. We do indeed see our custom attribute “article_changed_format” from our custom “article” normalizer:

When we look at the REST response of another content type (like a “page”), we do not see this custom attribute because it is not an “article” and the normalizer did not apply to it. It does, however, pick up the next normalizer in sequence, which happens to be the custom node normalizer we created earlier:

 

Notes

In determining how to implement a normalizer for your complex data, the normalizers in the “serialization” module serve as a great guide in learning how normalizers work and how different types of data are normalized.

The Serialization API documentation is also a great reference for how normalizers work in the serialization process.