Customising Views Serializer JSON Output I have created a Views Rest Export that looks like this:

{
   "id":"10",
   "title":"Plot 10",
},
{
   "id":"20",
   "title":"Plot 20",
}


But I would like to wrap some extra bits around it like this:

{
   "mapwidth":"690",
   "mapheight":"982",
   "levels":[
      {
         "id":"uk",
         "title":"United Kingdom",
         "locations":[
            {
               "id":"10",
               "title":"Plot 10",
            },
            {
               "id":"20",
               "title":"Plot 20",
            }
         ]
      }
   ]
}


I have created a php file with this in it, which seems to work:

{
    "mapwidth":"690",
    "mapheight":"982",
    "levels":[
        {
            "id":"uk",
            "title":"United Kingdom",
            "locations":
            &lt;?php $locations = file_get_contents('/api/plots?_format=json');
            echo $locations; ?&gt;
        }
    ]
}


But I was wondering what the better way of doing this would be.

Thanks in advance.
You can create a module and extend ContentEntityNormalizer to provide a custom structure.

Example:

&lt;?php

declare(strict_types = 1);

namespace Drupal\mymodule\Normalizer;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Drupal\serialization\Normalizer\ContentEntityNormalizer;
use Drupal\node\NodeInterface;

/**
 * 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',
  ];

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * {@inheritdoc}
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this-&gt;entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = []) {
    $type = $entity-&gt;getType();

    $methods = [
      'library' =&gt; 'normalizeLibrary',
      'collection' =&gt; 'normalizeCollection',
    ];

    if (isset($methods[$type])) {
      return $this-&gt;{$methods[$type]}($entity, $format, $context);
    }

    return parent::normalize($entity, $format, $context);
  }

  /**
   * Normalize a library content type.
   */
  protected function normalizeLibrary($entity) {
    $attributes['id']       = $entity-&gt;id();
    $attributes['featured'] = !empty($entity-&gt;get('field_is_coming_soon')-&gt;value);
    $attributes['external'] = !empty($entity-&gt;get('field_is_stub')-&gt;value) &amp;&amp; !empty($entity-&gt;get('field_website_link')[0]);
    $attributes['name']     = $entity-&gt;label();
    $attributes['subTitle'] = !empty($entity-&gt;get('field_subtitle')-&gt;value) ? $entity-&gt;get('field_subtitle')-&gt;value : '';

    return $attributes;
  }

  /**
   * Normalize a collection content type.
   *
   * @param \Drupal\node\NodeInterface $entity
   *   The node entity.
   *
   * @return array
   *   The item attributes.
   */
  protected function normalizeCollection(NodeInterface $entity) : array {
    $attributes = [];
    $attributes['id'] = $entity-&gt;id();
    $attributes['title'] = $entity-&gt;label();
    $attributes['subtitle'] = $entity-&gt;get('field_subtitle')-&gt;value;
    $attributes['summary'] = $entity-&gt;get('field_summary')-&gt;value;
    $attributes['card_type'] = $entity-&gt;get('field_directory_view')-&gt;value;
    $attributes['list_image'] = NULL;
    $attributes['card_image'] = NULL;
    $attributes['url'] = Url::fromRoute('entity.node.canonical', ['node' =&gt; $attributes['id']])-&gt;toString();

    $media = $entity-&gt;get('field_featured_image')-&gt;getValue();

    if (!empty($media)) {
      $item = $this-&gt;entityTypeManager-&gt;getStorage('media')-&gt;load($media[0]['target_id']);
      $image = $item-&gt;get('image')-&gt;getValue();

      $attributes['list_image'] = [
        'src' =&gt; ImageStyle::load('collection_listing')
          -&gt;buildUrl($item-&gt;image-&gt;entity-&gt;getFileUri()),
        'alt' =&gt; $image[0]['alt'],
      ];

      switch ($attributes['card_type']) {
        case 'wide':
          $attributes['card_image'] = [
            'src' =&gt; ImageStyle::load('collection_listing_wide')
              -&gt;buildUrl($item-&gt;image-&gt;entity-&gt;getFileUri()),
            'alt' =&gt; $image[0]['alt'],
          ];
          break;

        case 'tall':
          $attributes['card_image'] = [
            'src' =&gt; ImageStyle::load('collection_listing_tall')
              -&gt;buildUrl($item-&gt;image-&gt;entity-&gt;getFileUri()),
            'alt' =&gt; $image[0]['alt'],
          ];
          break;

        case 'standard':
          $attributes['card_image'] = $attributes['list_image'];
          break;

        default:
          break;
      }
    }

    return $attributes;
  }

}


This code only applies to Node entities, and only for two content types (you will want to specify them, since this will override anything matching the $supportedInterfaceOrClass property.

You will need to add it to your custom modules services.yml file:

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


The View will now use this normalizer if the conditions match and you can structure the output however you like. This gives you the power say, to reach into entity references and pull in values or do conditional things (like the custom image size I am passing in normalizeCollection depending on a field value).

Note that my example is custom and does not do things like check field level access for the user, since we do not have any restricted fields.