Automatically including all relationships in Drupal JSON:API

Automatically including all relationships in Drupal JSON:API cover image

Photo by Karine Avetisyan on Unsplash

There is currently no way to automatically include all relationships when fetching resources and this is actually intentional and by design.

It should be up to the client to explicitly define any additional data that it requires. However, there are scenarios where all data is really required and having the client manage this list of data to be included can be tedious and messy as new relationships are added.

Reference fields

There are certain field types that allow you to reference other entities (e.g. node, paragraph, etc), and these are the fields that creates relationships. The most common of these field types are:

  • Entity reference field
  • Entity reference revision field
  • Image field

We can then write code that checks if an entity's field is one that references another entity and automatically include the JSON:API of the entity that is being referenced.

Custom service

We create a custom service to make the code reusable. The code below recursively retrieves all relationships base on the type of the field and the type of the entity it references.

Dependencies

To serialize an entity object into JSON:API format, we rely on a helper function provided by the jsonapi_extras module.

my_module/src/JsonApiBuilder

<?php

namespace Drupal\my_module;

use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\jsonapi_extras\EntityToJsonApi;

/**
 * JsonApiBuilder service.
 */
class JsonApiBuilder {

  /**
   * The EntityToJsonApi service.
   *
   * @var \Drupal\jsonapi_extras\EntityToJsonApi
   */
  protected $entityToJsonApi;

  /**
   * The EntityFieldManager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * Constructs a JsonApiBuilder object.
   *
   * @param \Drupal\jsonapi_extras\EntityToJsonApi $entity_to_jsonapi
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   */
  public function __construct(EntityToJsonApi $entity_to_jsonapi, EntityFieldManagerInterface $entity_field_manager) {
    $this->entityToJsonApi = $entity_to_jsonapi;
    $this->entityFieldManager = $entity_field_manager;
  }

  /**
   * Method description.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *
   * @return string
   * @throws \Exception
   */
  public function buildJsonApi(EntityInterface $entity): string {
    $includes = $this->getIncludes($entity);
    return $this->entityToJsonApi->serialize($entity, $includes);
  }

  /**
   * Recursively retrieve an array of field paths that's suitable for the
   * include parameter of a JSON:API request.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *
   * @return array
   */
  public function getIncludes(EntityInterface $entity): array {
    // Field types that can reference other entities.
    $field_types = [
      'entity_reference_revisions',
      'entity_reference',
      'image',
    ];

    // Referencable entities that we want to include.
    $target_types = [
      'paragraph',
      'media',
      'file',
      'node',
    ];

    $includes = [];

    // Iterate the fields of an entity and look for fields that can reference
    // other entities.
    $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
    foreach ($field_definitions as $field_definition) {
      if (!$field_definition->getFieldStorageDefinition()->isBaseField()) {
        // Only include specific field types.
        if (in_array($field_definition->getType(), $field_types)) {
          // Only include specific entities.
          if (in_array($field_definition->getSetting('target_type'), $target_types)) {
            $includes[] = $field_definition->getName();

            // Get the referenced entities and also get their relationships.
            $referenced_entities = $entity->get($field_definition->getName())->referencedEntities();
            foreach ($referenced_entities as $referenced_entity) {
              $_includes = $this->getIncludes($referenced_entity);
              foreach ($_includes as $_include) {
                $full_path = $field_definition->getName() . '.' . $_include;
                if (!in_array($full_path, $includes)) {
                  $includes[] = $full_path;
                }
              }
            }
          }
        }
      }
    }

    return $includes;
  }

}

Don't forget to define the service in the services.yml file:

my_module/my_module.services.yml

services:
  my_module.jsonapi_builder:
    class: Drupal\my_module\JsonApiBuilder
    arguments: ['@jsonapi_extras.entity.to_jsonapi', '@entity_field.manager']

Usage

Now that we have service, accessing it can be done via dependency injection or via the generic \Drupal::service() method.

Example:

$jsonapi_builder = \Drupal::service('my_module.jsonapi_builder');
$output = $jsonapi_builder->buildJsonApi($entity);