diff --git a/core/modules/comment/src/Tests/Views/CommentRestExportTest.php b/core/modules/comment/src/Tests/Views/CommentRestExportTest.php
index 9737b4f..ad2197e 100644
--- a/core/modules/comment/src/Tests/Views/CommentRestExportTest.php
+++ b/core/modules/comment/src/Tests/Views/CommentRestExportTest.php
@@ -58,8 +58,8 @@ public function testCommentRestExport() {
     $this->drupalGetWithFormat(sprintf('node/%d/comments', $this->nodeUserCommented->id()), 'hal_json');
     $this->assertResponse(200);
     $contents = Json::decode($this->getRawContent());
-    $this->assertEqual($contents[0]['subject'], 'How much wood would a woodchuck chuck');
-    $this->assertEqual($contents[1]['subject'], 'A lot, apparently');
+    $this->assertEqual($contents['_embedded']['item'][0]['subject'], 'How much wood would a woodchuck chuck');
+    $this->assertEqual($contents['_embedded']['item'][1]['subject'], 'A lot, apparently');
     $this->assertEqual(count($contents), 2);
 
     // Ensure field-level access is respected - user shouldn't be able to see
diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml
index e817fbf..03b66ef 100644
--- a/core/modules/hal/hal.services.yml
+++ b/core/modules/hal/hal.services.yml
@@ -30,3 +30,8 @@ services:
     class: Drupal\hal\EventSubscriber\ExceptionHalJsonSubscriber
     tags:
       - { name: event_subscriber }
+  serializer.normalizer.collection.hal:
+    class: Drupal\hal\Normalizer\CollectionNormalizer
+    arguments: ['@rest.link_manager']
+    tags:
+      - { name: normalizer, priority: 10 }
diff --git a/core/modules/hal/src/Normalizer/CollectionNormalizer.php b/core/modules/hal/src/Normalizer/CollectionNormalizer.php
new file mode 100644
index 0000000..c66cc64
--- /dev/null
+++ b/core/modules/hal/src/Normalizer/CollectionNormalizer.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\CollectionNormalizer.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\rest\LinkManager\LinkManagerInterface;
+use Drupal\serialization\Collection;
+
+/**
+ * Converts the Drupal entity object structure to a HAL array structure.
+ */
+class CollectionNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\serialization\Collection';
+
+  /**
+   * The collection link manager.
+   *
+   * @var \Drupal\rest\LinkManager\CollectionLinkManagerInterface
+   */
+  protected $linkManager;
+
+  /**
+   * Constructs an CollectionNormalizer object.
+   *
+   * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager
+   *   The hypermedia link manager.
+   */
+  public function __construct(LinkManagerInterface $link_manager) {
+    $this->linkManager = $link_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    // Create the array of normalized properties, starting with the URI.
+    $normalized = [
+      '_links' => [
+        'self' => [
+          'href' => $object->getUri(),
+        ],
+      ],
+    ];
+
+    // $object is not type-hinted, so we cannot be sure this method will not be
+    // called on a non-Collection object, and there is no interface to check on.
+    if (!$object instanceof Collection) {
+      return $normalized;
+    }
+
+    // If we have additional hypermedia links add them here.
+    $links = $object->getLinks();
+    if (is_array($links) && count($links)) {
+      foreach ($links as $key => $link) {
+        $normalized['_links'][$key] = ['href' => $link];
+      }
+    }
+
+    // Add the list of items.
+    $link_relation = $this->linkManager->getCollectionItemRelation($object->getCollectionId());
+    $normalized['_embedded'][$link_relation] = $this->serializer->normalize($object->getItems(), $format, $context);
+
+    return $normalized;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Implement denormalization once normalization has settled.
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Implement denormalization once normalization has settled.
+   */
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    return FALSE;
+  }
+}
diff --git a/core/modules/hal/src/Tests/FileNormalizeTest.php b/core/modules/hal/src/Tests/FileNormalizeTest.php
index 64ffd07..30a1134 100644
--- a/core/modules/hal/src/Tests/FileNormalizeTest.php
+++ b/core/modules/hal/src/Tests/FileNormalizeTest.php
@@ -15,6 +15,7 @@
 use Drupal\rest\LinkManager\RelationLinkManager;
 use Drupal\rest\LinkManager\TypeLinkManager;
 use Symfony\Component\Serializer\Serializer;
+use Drupal\rest\LinkManager\CollectionLinkManager;
 
 
 /**
@@ -39,7 +40,7 @@ protected function setUp() {
     $this->installEntitySchema('file');
 
     $entity_manager = \Drupal::entityManager();
-    $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new RelationLinkManager(new MemoryBackend('default'), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')));
+    $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new RelationLinkManager(new MemoryBackend('default'), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new CollectionLinkManager());
 
     // Set up the mock serializer.
     $normalizers = array(
diff --git a/core/modules/hal/src/Tests/NormalizerTestBase.php b/core/modules/hal/src/Tests/NormalizerTestBase.php
index 104a5e2..6f72f05 100644
--- a/core/modules/hal/src/Tests/NormalizerTestBase.php
+++ b/core/modules/hal/src/Tests/NormalizerTestBase.php
@@ -22,6 +22,7 @@
 use Drupal\serialization\EntityResolver\UuidResolver;
 use Drupal\simpletest\KernelTestBase;
 use Symfony\Component\Serializer\Serializer;
+use Drupal\rest\LinkManager\CollectionLinkManager;
 
 /**
  * Test the HAL normalizer.
@@ -135,7 +136,10 @@ protected function setUp() {
     ))->save();
 
     $entity_manager = \Drupal::entityManager();
-    $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new RelationLinkManager(new MemoryBackend('default'), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')));
+    $link_manager = new LinkManager(
+      new TypeLinkManager(new MemoryBackend('default'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')),
+      new RelationLinkManager(new MemoryBackend('default'), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')),
+      new CollectionLinkManager());
 
     $chain_resolver = new ChainEntityResolver(array(new UuidResolver($entity_manager), new TargetIdResolver()));
 
diff --git a/core/modules/hal/tests/src/Unit/CollectionNormalizerTest.php b/core/modules/hal/tests/src/Unit/CollectionNormalizerTest.php
new file mode 100644
index 0000000..7953815
--- /dev/null
+++ b/core/modules/hal/tests/src/Unit/CollectionNormalizerTest.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\hal\Tests\CollectionNormalizerTest.
+ */
+
+namespace Drupal\Tests\hal\Unit;
+
+use Drupal\hal\Normalizer\CollectionNormalizer;
+use Drupal\serialization\Collection;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the CollectionNormalizer's normalize supports.
+ *
+ * @coversDefaultClass \Drupal\hal\Normalizer\CollectionNormalizer
+ * @group HAL
+ */
+class CollectionNormalizerTest extends UnitTestCase {
+
+  /**
+   * Tests the supportsNormalization method.
+   */
+  public function testSupportsNormalization() {
+    $collection = $this->getCollection();
+    $normalizer = new CollectionNormalizer($this->getLinkManagerStub());
+    $this->assertTrue($normalizer->supportsNormalization($collection, 'hal_json'));
+    $this->assertFalse($normalizer->supportsNormalization($collection, 'json'));
+    $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), 'hal_json'));
+  }
+
+  /**
+   * Tests the normalize method.
+   */
+  public function testNormalize() {
+    $test_values = $this->getTestValues();
+    $collection = $this->getCollection();
+
+    // Create the normalizer and inject the LinkManagerStub.
+    $normalizer = new CollectionNormalizer($this->getLinkManagerStub());
+    // Inject the Serializer. Handle the call to Serializer::normalize,
+    // ensuring that the items array is passed in.
+    $serializer = $this->getSerializerStub();
+    $serializer->expects($this->any())
+      ->method('normalize')
+      ->with($collection->getItems())
+      ->will($this->returnValue($test_values['items']));
+    $normalizer->setSerializer($serializer);
+    // Get the normalized array.
+    $normalized = $normalizer->normalize($collection, 'hal_json');
+
+    // Test that self link points to collection URI.
+    $this->assertEquals($normalized['_links']['self']['href'], $test_values['uri']);
+    // There should only be a self-key.
+    $this->assertEquals(array_keys($normalized['_links']), array('self'));
+
+    // Test that the correct link relation was retrieved from the LinkManager
+    // and added to _embedded.
+    $this->assertArrayHasKey($test_values['item_link_relation'], $normalized['_embedded']);
+    // Test that the item link relation points to the serialized item array.
+    $this->assertEquals($normalized['_embedded'][$test_values['item_link_relation']], $test_values['items']);
+  }
+
+  /**
+   * Tests the normalize method on pageable collection.
+   */
+  public function testNormalizePageableCollection() {
+    $test_values = $this->getTestValues();
+    $collection = $this->getPageableCollection();
+
+    // Create the normalizer and inject the LinkManagerStub.
+    $normalizer = new CollectionNormalizer($this->getLinkManagerStub());
+    // Inject the Serializer. Handle the call to Serializer::normalize,
+    // ensuring that the items array is passed in.
+    $serializer = $this->getSerializerStub();
+    $serializer->expects($this->any())
+      ->method('normalize')
+      ->with($collection->getItems())
+      ->will($this->returnValue($test_values['items']));
+    $normalizer->setSerializer($serializer);
+    // Get the normalized array.
+    $normalized = $normalizer->normalize($collection, 'hal_json');
+
+    // Test that self link points to collection URI.
+    $this->assertEquals($normalized['_links']['self']['href'], $test_values['uri']);
+    // There should be the self-key, _first, _prev, _next and _last-keys.
+    $this->assertArrayHasKey('self', $normalized['_links']);
+    $this->assertArrayHasKey('first', $normalized['_links']);
+    $this->assertArrayHasKey('prev', $normalized['_links']);
+    $this->assertArrayHasKey('next', $normalized['_links']);
+    $this->assertArrayHasKey('last', $normalized['_links']);
+    $this->assertEquals(array_keys($normalized['_links']),
+      array('self', 'first', 'prev', 'next', 'last')
+    );
+
+    // Test that the correct link relation was retrieved from the LinkManager
+    // and added to _embedded.
+    $this->assertArrayHasKey($test_values['item_link_relation'], $normalized['_embedded']);
+    // Test that the item link relation points to the serialized item array.
+    $this->assertEquals($normalized['_embedded'][$test_values['item_link_relation']], $test_values['items']);
+  }
+
+  /**
+   * Get an \Drupal\serialization\Collection for testing.
+   *
+   * @return \Drupal\serialization\Collection
+   *   The Collection object, configured with test values.
+   */
+  protected function getCollection() {
+    $test_values = $this->getTestValues();
+
+    // Get a mock node.
+    $node = $this->getMockBuilder('Drupal\node\Entity\Node')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $collection = new Collection('test_id');
+    $collection->setUri($test_values['uri']);
+    $collection->setItems(array($node));
+
+    return $collection;
+  }
+
+  /**
+   * Get an pageable \Drupal\serialization\Collection for testing.
+   *
+   * @return \Drupal\serialization\Collection
+   *   The Collection object, configured with test values.
+   */
+  protected function getPageableCollection() {
+    $test_values = $this->getTestValues();
+
+    // Get a dummy node.
+    $node = $this->getMockBuilder('Drupal\node\Entity\Node')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $collection = new Collection('test_id');
+    $collection->setUri($test_values['uri']);
+    $collection->setItems(array($node));
+
+    $collection->setLinks(array(
+      'first' => $test_values['uri'] . '?page=0',
+      'prev' => $test_values['uri'] . '?page=0',
+      'next' => $test_values['uri'] . '?page=2',
+      'last' => $test_values['uri'] . '?page=2',
+    ));
+
+    return $collection;
+  }
+
+  /**
+   * Get a stub LinkManager for testing.
+   *
+   * @return \Drupal\rest\LinkManager\LinkManagerInterface
+   *   The LinkManager stub.
+   */
+  protected function getLinkManagerStub() {
+    $test_values = $this->getTestValues();
+
+    $link_manager = $this->getMockBuilder('Drupal\rest\LinkManager\LinkManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $link_manager->expects($this->any())
+      ->method('getCollectionItemRelation')
+      ->will($this->returnValue($test_values['item_link_relation']));
+
+    return $link_manager;
+  }
+
+  /**
+   * Get a stub Serializer for testing.
+   *
+   * @return \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Normalizer\NormalizerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface|\Symfony\Component\Serializer\Encoder\EncoderInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface|\PHPUnit_Framework_MockObject_MockObject
+   *   The Serializer mock.
+   */
+  protected function getSerializerStub() {
+    $serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    return $serializer;
+  }
+
+  /**
+   * Get the array of test values.
+   *
+   * @return array
+   *   An array of test values, used for configuring stub methods and testing.
+   */
+  protected function getTestValues() {
+    return array(
+      'item_link_relation' => 'item',
+      'items' => 'Array of serialized entities goes here',
+      'uri' => 'http://example.com/test-path',
+    );
+  }
+}
diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml
index 6b613e3..d8caa26 100644
--- a/core/modules/rest/rest.services.yml
+++ b/core/modules/rest/rest.services.yml
@@ -15,13 +15,15 @@ services:
       - { name: access_check }
   rest.link_manager:
     class: Drupal\rest\LinkManager\LinkManager
-    arguments: ['@rest.link_manager.type', '@rest.link_manager.relation']
+    arguments: ['@rest.link_manager.type', '@rest.link_manager.relation', '@rest.link_manager.collection']
   rest.link_manager.type:
     class: Drupal\rest\LinkManager\TypeLinkManager
     arguments: ['@cache.default', '@module_handler', '@config.factory', '@request_stack']
   rest.link_manager.relation:
     class: Drupal\rest\LinkManager\RelationLinkManager
     arguments: ['@cache.default', '@entity.manager', '@module_handler', '@config.factory', '@request_stack']
+  rest.link_manager.collection:
+    class: Drupal\rest\LinkManager\CollectionLinkManager
   rest.resource_routes:
     class: Drupal\rest\Routing\ResourceRoutes
     arguments: ['@plugin.manager.rest', '@config.factory', '@logger.channel.rest']
diff --git a/core/modules/rest/src/LinkManager/CollectionLinkManager.php b/core/modules/rest/src/LinkManager/CollectionLinkManager.php
new file mode 100644
index 0000000..2175a0c
--- /dev/null
+++ b/core/modules/rest/src/LinkManager/CollectionLinkManager.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\CollectionLinkManager.
+ */
+
+namespace Drupal\rest\LinkManager;
+
+/**
+ * Default collection link relation mapper.
+ */
+class CollectionLinkManager implements CollectionLinkManagerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCollectionItemRelation($collection_id) {
+    // By default, use the item IANA Link Relation, which is a generic way to
+    // link to items from a collection.
+    // @see http://tools.ietf.org/html/rfc6573.
+    return 'item';
+  }
+
+}
diff --git a/core/modules/rest/src/LinkManager/CollectionLinkManagerInterface.php b/core/modules/rest/src/LinkManager/CollectionLinkManagerInterface.php
new file mode 100644
index 0000000..e51924c
--- /dev/null
+++ b/core/modules/rest/src/LinkManager/CollectionLinkManagerInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\CollectionLinkManagerInterface.
+ */
+
+namespace Drupal\rest\LinkManager;
+
+/**
+ * Interface for mapping collection (e.g. Views) link relations.
+ */
+interface CollectionLinkManagerInterface {
+
+  /**
+   * Get link relations to items in a collection.
+   *
+   * @param string $collection_id
+   *   The identifier of a collection (e.g. View name).
+   *
+   * @return string
+   *   The link relation.
+   */
+  public function getCollectionItemRelation($collection_id);
+}
diff --git a/core/modules/rest/src/LinkManager/LinkManager.php b/core/modules/rest/src/LinkManager/LinkManager.php
index 714e70b..c4aaaeb 100644
--- a/core/modules/rest/src/LinkManager/LinkManager.php
+++ b/core/modules/rest/src/LinkManager/LinkManager.php
@@ -23,16 +23,26 @@ class LinkManager implements LinkManagerInterface {
   protected $relationLinkManager;
 
   /**
+   * The collection link manager.
+   *
+   * @var \Drupal\rest\LinkManager\CollectionLinkManagerInterface
+   */
+  protected $collectionLinkManager;
+
+  /**
    * Constructor.
    *
    * @param \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_link_manager
-   *   Manager for handling bundle URIs.
+   *   Manager for handling type links corresponding to bundles.
    * @param \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_link_manager
-   *   Manager for handling bundle URIs.
+   *   Manager for handling link relations corresponding to fields.
+   * @param \Drupal\rest\LinkManager\CollectionLinkManagerInterface $collection_link_manager
+   *   Manager for handling collection links.
    */
-  public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager) {
+  public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager, CollectionLinkManagerInterface $collection_link_manager) {
     $this->typeLinkManager = $type_link_manager;
     $this->relationLinkManager = $relation_link_manager;
+    $this->collectionLinkManager = $collection_link_manager;
   }
 
   /**
@@ -72,4 +82,12 @@ public function setLinkDomain($domain) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCollectionItemRelation($collection_id) {
+    return $this->collectionLinkManager->getCollectionItemRelation($collection_id);
+  }
+
 }
+
diff --git a/core/modules/rest/src/LinkManager/LinkManagerInterface.php b/core/modules/rest/src/LinkManager/LinkManagerInterface.php
index 26c3bb8..7859279 100644
--- a/core/modules/rest/src/LinkManager/LinkManagerInterface.php
+++ b/core/modules/rest/src/LinkManager/LinkManagerInterface.php
@@ -19,5 +19,5 @@
  * custom logic, it is expected to be more common for plugin managers to proxy
  * the method invocations to the respective components.
  */
-interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface {
+interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface, CollectionLinkManagerInterface {
 }
diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php
index 68bd407..41a0fac 100644
--- a/core/modules/rest/src/Plugin/views/display/RestExport.php
+++ b/core/modules/rest/src/Plugin/views/display/RestExport.php
@@ -256,7 +256,7 @@ public function optionsSummary(&$categories, &$options) {
    * {@inheritdoc}
    */
   public function collectRoutes(RouteCollection $collection) {
-    parent::collectRoutes($collection);
+    $result = parent::collectRoutes($collection);
     $view_id = $this->view->storage->id();
     $display_id = $this->display['id'];
 
@@ -274,6 +274,7 @@ public function collectRoutes(RouteCollection $collection) {
         $route->setRequirement('_format', implode('|', $formats + ['html']));
       }
     }
+    return $result;
   }
 
   /**
diff --git a/core/modules/rest/src/Plugin/views/style/Serializer.php b/core/modules/rest/src/Plugin/views/style/Serializer.php
index d9e7274..4fb7b36 100644
--- a/core/modules/rest/src/Plugin/views/style/Serializer.php
+++ b/core/modules/rest/src/Plugin/views/style/Serializer.php
@@ -13,6 +13,9 @@
 use Drupal\views\Plugin\views\style\StylePluginBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Serializer\SerializerInterface;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\serialization\Collection;
 
 /**
  * The style plugin for serialized output formats.
@@ -50,7 +53,21 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
    *
    * @var array
    */
-  protected $formats = array();
+  protected $formats = [];
+
+  /**
+   * The URL generator service.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * The state service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
 
   /**
    * {@inheritdoc}
@@ -61,19 +78,38 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_id,
       $plugin_definition,
       $container->get('serializer'),
-      $container->getParameter('serializer.formats')
+      $container->getParameter('serializer.formats'),
+      $container->get('url_generator'),
+      $container->get('state')
     );
   }
 
   /**
-   * Constructs a Plugin object.
+   * Serializer constructor.
+   *
+   * @param array $configuration
+   *
+   * @param string $plugin_id
+   *
+   * @param array $plugin_definition
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *
+   * @param array $serializer_formats
+   *
+   * @param UrlGeneratorInterface $url_generator
+   *   The URL generator service.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, array $serializer_formats) {
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, SerializerInterface $serializer, array $serializer_formats, UrlGeneratorInterface $url_generator, StateInterface $state) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->definition = $plugin_definition + $configuration;
     $this->serializer = $serializer;
     $this->formats = $serializer_formats;
+    $this->urlGenerator = $url_generator;
+    $this->state = $state;
   }
 
   /**
@@ -115,27 +151,7 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function render() {
-    $rows = array();
-    // If the Data Entity row plugin is used, this will be an array of entities
-    // which will pass through Serializer to one of the registered Normalizers,
-    // which will transform it to arrays/scalars. If the Data field row plugin
-    // is used, $rows will not contain objects and will pass directly to the
-    // Encoder.
-    foreach ($this->view->result as $row_index => $row) {
-      $this->view->row_index = $row_index;
-      $rows[] = $this->view->rowPlugin->render($row);
-    }
-    unset($this->view->row_index);
-
-    // Get the content type configured in the display or fallback to the
-    // default.
-    if ((empty($this->view->live_preview))) {
-      $content_type = $this->displayHandler->getContentType();
-    }
-    else {
-      $content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
-    }
-    return $this->serializer->serialize($rows, $content_type);
+    return $this->serializer->serialize($this->getCollection(), $this->displayHandler->getContentType());
   }
 
   /**
@@ -172,4 +188,90 @@ public function getCacheTags() {
     return [];
   }
 
+  /**
+   * Instantiate Collection object needed to encapsulate serialization.
+   *
+   * @return \Drupal\serialization\Collection
+   *   Collection object wrapping items/rows of the view.
+   */
+  public function getCollection() {
+
+    $display = $this->view->getDisplay();
+    // Build full view-display id.
+    $view_id = $this->view->storage->id();
+    $display_id = $display->display['id'];
+
+    // Instantiate collection object.
+    $collection = new Collection($view_id . '_' . $display_id);
+
+    $collection->setTitle($this->view->getTitle());
+    $collection->setDescription($display->getOption('display_description'));
+
+    // Route as defined in e.g. \Drupal\rest\Plugin\views\display\RestExport.
+    $route_names = $this->state->get('views.view_route_names');
+    $route_name = $route_names["$view_id.$display_id"];
+
+    // Get base url path for the view; getUrl returns a path not an absolute
+    // URL (and no page information).
+    /** @var \Drupal\Core\Url $view_base_url */
+    $view_base_url = $this->view->getUrl();
+
+    // Inject the page into the canonical URI of the view.
+    if ($this->view->getCurrentPage() > 0) {
+      $uri = $this->urlGenerator->generateFromRoute($route_name, $view_base_url->getRouteParameters(), array('query' => array('page' => $this->view->getCurrentPage()), 'absolute' => TRUE));
+    }
+    else {
+      $uri = $this->urlGenerator->generateFromRoute($route_name, $view_base_url->getRouteParameters(), array('absolute' => TRUE));
+    }
+    $collection->setUri($uri);
+
+    $rows = [];
+    foreach ($this->view->result as $row_index => $row) {
+      $this->view->row_index = $row_index;
+      $rows[] = $this->view->rowPlugin->render($row);
+    }
+
+    unset($this->view->row_index);
+    $collection->setItems($rows);
+
+    $pager = $this->view->getPager();
+
+    // Determine whether we have more items than we are showing, in that case
+    // we are a pageable collection.
+    if ($pager->getTotalItems() > $pager->getItemsPerPage()) {
+      // Calculate pager links.
+      $current_page = $pager->getCurrentPage();
+      // Starting at page=0 we need to decrement.
+      $total = ceil($pager->getTotalItems() / $pager->getItemsPerPage()) - 1;
+
+      $collection->setLink('first', $this->urlGenerator->generateFromRoute($route_name, [], array(
+        'query' => array('page' => 0),
+        'absolute' => TRUE,
+      )));
+
+      // If we are not on the first page add a previous link.
+      if ($current_page > 0) {
+        $collection->setLink('prev', $this->urlGenerator->generateFromRoute($route_name, [], array(
+          'query' => array('page' => $current_page - 1),
+          'absolute' => TRUE,
+        )));
+      }
+
+      // If we are not on the last page add a next link.
+      if ($current_page < $total) {
+        $collection->setLink('next', $this->urlGenerator->generateFromRoute($route_name, [], array(
+          'query' => array('page' => $current_page + 1),
+          'absolute' => TRUE,
+        )));
+      }
+
+      $collection->setLink('last', $this->urlGenerator->generateFromRoute($route_name, [], array(
+        'query' => array('page' => $total),
+        'absolute' => TRUE,
+      )));
+    }
+
+    return $collection;
+  }
+
 }
diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
index e3b8d26..50cec6a 100644
--- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
+++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
@@ -7,14 +7,17 @@
 
 namespace Drupal\rest\Tests\Views;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\views\Entity\View;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\ViewExecutable;
 use Drupal\views\Views;
 use Drupal\views\Tests\Plugin\PluginTestBase;
 use Drupal\views\Tests\ViewTestData;
@@ -73,6 +76,182 @@ protected function setUp() {
   }
 
   /**
+   * Retrieves a Drupal path or an absolute path with hal+json.
+   *
+   * Sets Accept-Header to application/hal+json and decodes the result.
+   *
+   * @param string $path
+   *   Path to request HAL+JSON from.
+   * @param array $options
+   *   Array of options to pass to the URL generator.
+   * @param array $headers
+   *   Array of headers.
+   *
+   * @return array
+   *   Decoded json.
+   * Requests a Drupal path in HAL+JSON format, and JSON decodes the response.
+   */
+  protected function drupalGetHalJson($path, array $options = array(), array $headers = array()) {
+    $headers[] = 'Accept: application/hal+json';
+    return Json::decode($this->drupalGet($path, $options, $headers));
+  }
+
+  /**
+   * Retrieve the Collection object for given view.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The View being executed.
+   *
+   * @return \Drupal\serialization\Collection
+   *   The collection object to pass into the serializer.
+   */
+  protected function getCollectionFromView(ViewExecutable $view) {
+    return $view->getStyle()->getCollection();
+  }
+
+
+
+  /**
+   * Tests the Serializer paths and responses for field-based views.
+   */
+  public function testSerializerFieldDisplayResponse() {
+
+    $view = Views::getView('test_serializer_display_field');
+    $view->setDisplay('rest_export_1');
+    // Mock the request content type by setting it on the display handler.
+    $view->display_handler->setContentType('hal_json');
+    $this->executeView($view);
+
+    $view_output = $view->preview();
+    $view_result = array();
+    foreach ($view->result as $row) {
+      $expected_row = array();
+      foreach ($view->field as $id => $field) {
+        $expected_row[$id] = $field->render($row);
+      }
+      $view_result[] = $expected_row;
+    }
+
+    $serializer = $this->container->get('serializer');
+    $collection = $this->getCollectionFromView($view);
+    $expected = $serializer->serialize($collection, 'hal_json');
+    $actual_json = $this->drupalGetWithFormat('test/serialize/field', 'hal_json');
+
+    $this->assertIdentical($actual_json, $view_output['#markup']->jsonSerialize(), 'Preview output matches the (reserialized) JSON returned from the view via HTTP GET.');
+    $this->assertIdentical($actual_json, $expected, 'HAL serializer output matches the (reserialized) JSON returned from the view via HTTP GET.');
+    // @TODO needs review. the structure is not the same at all.
+    //$this->assertIdentical($actual_json['_embedded']['item'], $view_result, 'View result matches JSON returned from the view via HTTP GET');
+  }
+
+  /**
+   * Tests the Serializer paths and responses for field-based views with paging.
+   */
+  public function testSerializerFieldDisplayPagingResponse() {
+    $view = Views::getView('test_serializer_display_field');
+    $view->setDisplay('rest_export_paging');
+    // Mock the request content type by setting it on the display handler.
+    $view->display_handler->setContentType('hal_json');
+    $this->executeView($view);
+
+    $view_output = $view->preview();
+    $view_result = array();
+    foreach ($view->result as $row) {
+      $expected_row = array();
+      foreach ($view->field as $id => $field) {
+        $expected_row[$id] = $field->render($row);
+      }
+      $view_result[] = $expected_row;
+    }
+
+    $serializer = $this->container->get('serializer');
+    $collection = $this->getCollectionFromView($view);
+    $expected = $serializer->serialize($collection, 'hal_json');
+
+    $actual_json = $this->drupalGetWithFormat('test/serialize/field-paging', 'hal_json');
+    $actual_json_decoded = Json::decode($actual_json);
+    $this->assertIdentical($actual_json, $view_output['#markup']->jsonSerialize(), 'Preview output matches the (reserialized) JSON returned from the view via HTTP GET.');
+    $this->assertIdentical($actual_json, $expected, 'HAL serializer output matches the (reserialized) JSON returned from the view via HTTP GET.');
+    // @TODO Remove? the structure is not the same at all.
+    //$this->assertIdentical(Json::decode($actual_json)['_embedded']['item'], $view_result, 'View result matches JSON returned from the view via HTTP GET');
+
+    // Make assertions on the structure of the response.
+    $this->assertTrue(isset($actual_json_decoded['_embedded']) && isset($actual_json_decoded['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json_decoded['_embedded']['item']), 1);
+    $this->assertEqual($actual_json_decoded['_links']['self']['href'], $this->viewUrl($view));
+    $this->assertEqual($actual_json_decoded['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_decoded['_links']['next']['href'], $this->viewUrl($view, 1));
+    $this->assertEqual($actual_json_decoded['_links']['last']['href'], $this->viewUrl($view, 4));
+    $this->assertEqual(array_keys($actual_json_decoded['_links']), array(
+      'self',
+      'first',
+      'next',
+      'last',
+    ));
+
+    // Load the second page.
+    $actual_json_page_1 = $this->drupalGetHalJson($actual_json_decoded['_links']['next']['href']);
+
+    $this->assertTrue(isset($actual_json_page_1['_embedded']) && isset($actual_json_page_1['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json_page_1['_embedded']['item']), 1);
+    $this->assertEqual($actual_json_page_1['_links']['self']['href'], $this->viewUrl($view, 1));
+    $this->assertEqual($actual_json_page_1['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_1['_links']['prev']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_1['_links']['next']['href'], $this->viewUrl($view, 2));
+    $this->assertEqual($actual_json_page_1['_links']['last']['href'], $this->viewUrl($view, 4));
+    $this->assertEqual(array_keys($actual_json_page_1['_links']), array(
+      'self',
+      'first',
+      'prev',
+      'next',
+      'last',
+    ));
+
+    // Test the entity rows - with paging.
+    $view = Views::getView('test_serializer_display_field');
+    $view->setDisplay('rest_export_paging');
+    $view->setCurrentPage(1);
+    $this->executeView($view);
+
+    // Create the entity collection.
+    $collection = $this->getCollectionFromView($view);
+    $this->assertTrue($collection->hasLinks(), 'Collection created from a paging view has (hypermedia) link relations');
+    $expected = $serializer->serialize($collection, 'hal_json');
+
+    $this->assertEqual(Json::encode($actual_json_page_1), $expected, 'The expected HAL output for page=1 was found.');
+
+    // Load the last page.
+    $actual_json_page_last = $this->drupalGetWithFormat($actual_json_decoded['_links']['last']['href'], 'hal_json');//$this->drupalGetHalJson($actual_json_decoded['_links']['last']['href']);
+    $actual_json_page_last_decoded = Json::decode($actual_json_page_last);
+    $this->assertTrue(isset($actual_json_page_last_decoded['_embedded']) && isset($actual_json_page_last_decoded['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json_page_last_decoded['_embedded']['item']), 1);
+    $this->assertEqual($actual_json_page_last_decoded['_links']['self']['href'], $this->viewUrl($view, 4));
+    $this->assertEqual($actual_json_page_last_decoded['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_last_decoded['_links']['prev']['href'], $this->viewUrl($view, 3));
+    $this->assertEqual($actual_json_page_last_decoded['_links']['last']['href'], $this->viewUrl($view, 4));
+    $this->assertEqual(array_keys($actual_json_page_last_decoded['_links']), array(
+      'self',
+      'first',
+      'prev',
+      'last',
+    ));
+
+    // Test the entity rows - with paging.
+    $view = Views::getView('test_serializer_display_field');
+    $view->setDisplay('rest_export_paging');
+    $view->setCurrentPage(4);
+    $this->executeView($view);
+
+    // Create the entity collection.
+    $collection = $this->getCollectionFromView($view);
+    $this->assertTrue($collection->hasLinks(), 'Collection created from a paging view has (hypermedia) link relations');
+
+    $expected = $serializer->serialize($collection, 'hal_json');
+    $this->assertEqual($actual_json_page_last, $expected, 'The expected HAL output for last page was found.');
+  }
+  /**
    * Checks the behavior of the Serializer callback paths and row plugins.
    */
   public function testSerializerResponses() {
@@ -118,7 +297,7 @@ public function testSerializerResponses() {
 
     // Test the entity rows.
     $view = Views::getView('test_serializer_display_entity');
-    $view->initDisplay();
+    $view->setDisplay('rest_export_1');
     $this->executeView($view);
 
     // Get the serializer service.
@@ -143,11 +322,22 @@ public function testSerializerResponses() {
     $this->assertCacheTags($expected_cache_tags);
     $this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
 
-    $expected = $serializer->serialize($entities, 'hal_json');
+    // Create the entity collection.
+    $collection = $this->getCollectionFromView($view);
+    $expected = $serializer->serialize($collection, 'hal_json');
     $actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json');
     $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
     $this->assertCacheTags($expected_cache_tags);
 
+    // Make assertions on the structure of the response.
+    $actual_json_decoded = JSON::decode($actual_json);
+    $this->assertTrue(isset($actual_json_decoded['_embedded']), 'Has _embedded key.');
+    $this->assertTrue(isset($actual_json_decoded['_links']), 'Has _links key.');
+
+    $this->assertEqual(count($actual_json_decoded['_embedded']['item']), 10);
+    $this->assertEqual($actual_json_decoded['_links']['self']['href'], $this->viewUrl($view));
+    $this->assertEqual(array_keys($actual_json_decoded['_links']), ['self']);
+
     // Change the default format to xml.
     $view->setDisplay('rest_export_1');
     $view->getDisplay()->setOption('style', array(
@@ -187,6 +377,110 @@ public function testSerializerResponses() {
   }
 
   /**
+   * Build an absolute URL from a view path, accounting for paging.
+   *
+   * @param ViewExecutable $view
+   *   The View being executed.
+   * @param null $page
+   *
+   * @return string
+   */
+  protected function viewUrl(ViewExecutable $view, $page = NULL) {
+    $base_url = 'base://'  . $view->getPath();
+    $options = array(
+      'absolute' => TRUE,
+    );
+    if (isset($page)) {
+      $options += array('query' => array('page' => $page));
+    }
+    return Url::fromUri($base_url, $options)->toString();
+  }
+
+  /**
+   * Checks the paging behavior of callback paths and row plugins.
+   */
+  protected function testSerializerPageableCollectionHalJsonResponses() {
+    // Test the entity rows - with paging.
+    $view = Views::getView('test_serializer_display_entity');
+    $view->setDisplay('rest_export_paging');
+    $this->executeView($view);
+
+    // Get the serializer service.
+    $serializer = $this->container->get('serializer');
+
+    // Create the entity collection from the current view.
+    $collection = $this->getCollectionFromView($view);
+    $this->assertTrue($collection->hasLinks(), 'Collection created from a paging view has (hypermedia) link relations');
+
+    $expected = $serializer->serialize($collection, 'hal_json');
+    $actual_json = $this->drupalGetHalJson('test/serialize/entity_paging');
+    $this->assertIdentical(Json::encode($actual_json), $expected, 'The expected HAL output was found.');
+
+    // Make assertions on the structure of the response.
+    $this->assertTrue(isset($actual_json['_embedded']) && isset($actual_json['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json['_embedded']['item']), 1);
+    $this->assertEqual($actual_json['_links']['self']['href'], $this->viewUrl($view));
+    $this->assertEqual($actual_json['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json['_links']['next']['href'], $this->viewUrl($view, 1));
+    $this->assertEqual($actual_json['_links']['last']['href'], $this->viewUrl($view, 9));
+    $this->assertEqual(array_keys($actual_json['_links']), array(
+      'self',
+      'first',
+      'next',
+      'last',
+    ));
+
+    // Load the second page.
+    $actual_json_page_1 = $this->drupalGetHalJson($actual_json['_links']['next']['href']);
+    $this->assertTrue(isset($actual_json_page_1['_embedded']) && isset($actual_json_page_1['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json_page_1['_embedded']['item']), 1);
+    $this->assertEqual($actual_json_page_1['_links']['self']['href'], $this->viewUrl($view, 1));
+    $this->assertEqual($actual_json_page_1['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_1['_links']['prev']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_1['_links']['next']['href'], $this->viewUrl($view, 2));
+    $this->assertEqual($actual_json_page_1['_links']['last']['href'], $this->viewUrl($view, 9));
+    $this->assertEqual(array_keys($actual_json_page_1['_links']), array(
+      'self',
+      'first',
+      'prev',
+      'next',
+      'last',
+    ));
+
+    // Test the entity rows - with paging.
+    $view = Views::getView('test_serializer_display_entity');
+    $view->setDisplay('rest_export_paging');
+    $view->setCurrentPage(1);
+    $this->executeView($view);
+
+    // Create the entity collection.
+    $collection = $this->getCollectionFromView($view);
+    $this->assertTrue($collection->hasLinks(), 'Collection created from a paging view has (hypermedia) link relations');
+    $expected = $serializer->serialize($collection, 'hal_json');
+
+    $this->assertEqual(Json::encode($actual_json_page_1), $expected, 'The expected HAL output for page=1 was found.');
+
+    // Load the last page.
+    $actual_json_page_last = $this->drupalGetHalJSON($actual_json['_links']['last']['href']);
+
+    $this->assertTrue(isset($actual_json_page_last['_embedded']) && isset($actual_json_page_last['_links']), 'Has _links and _embedded keys');
+
+    $this->assertEqual(count($actual_json_page_last['_embedded']['item']), 1);
+    $this->assertEqual($actual_json_page_last['_links']['self']['href'], $this->viewUrl($view, 9));
+    $this->assertEqual($actual_json_page_last['_links']['first']['href'], $this->viewUrl($view, 0));
+    $this->assertEqual($actual_json_page_last['_links']['prev']['href'], $this->viewUrl($view, 8));
+    $this->assertEqual($actual_json_page_last['_links']['last']['href'], $this->viewUrl($view, 9));
+    $this->assertEqual(array_keys($actual_json_page_last['_links']), array(
+      'self',
+      'first',
+      'prev',
+      'last',
+    ));
+  }
+
+  /**
    * Sets up a request on the request stack with a specified format.
    *
    * @param string $format
@@ -462,7 +756,7 @@ public function testFieldRawOutput() {
    * Tests the live preview output for json output.
    */
   public function testLivePreview() {
-    // We set up a request so it looks like an request in the live preview.
+    // We set up a request so it looks like a request in the live preview.
     $request = new Request();
     $request->setFormat('drupal_ajax', 'application/vnd.drupal-ajax');
     $request->headers->set('Accept', 'application/vnd.drupal-ajax');
@@ -472,6 +766,7 @@ public function testLivePreview() {
 
     $view = Views::getView('test_serializer_display_entity');
     $view->setDisplay('rest_export_1');
+    $view->display_handler->setContentType('json');
     $this->executeView($view);
 
     // Get the serializer service.
@@ -495,20 +790,15 @@ public function testLivePreview() {
 
     // Change the request format to xml.
     $view->setDisplay('rest_export_1');
-    $view->getDisplay()->setOption('style', array(
-      'type' => 'serializer',
-      'options' => array(
-        'uses_fields' => FALSE,
-        'formats' => array(
-          'xml' => 'xml',
-        ),
-      ),
-    ));
+    $view->display_handler->setContentType('xml');
 
     $this->executeView($view);
     $build = $view->preview();
     $rendered_xml = $build['#plain_text'];
     $this->assertEqual($rendered_xml, $expected, 'Ensure we preview xml when we change the request format.');
+
+    // @TODO add HAL+JSON and paging.
+
   }
 
   /**
diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_entity.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_entity.yml
index 2a5818e..2eb605a 100644
--- a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_entity.yml
+++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_entity.yml
@@ -53,3 +53,24 @@ display:
       defaults:
         access: false
       path: test/serialize/entity
+  rest_export_paging:
+    display_plugin: rest_export
+    id: rest_export_paging
+    display_title: serializer
+    position: null
+    display_options:
+      defaults:
+        access: false
+      path: test/serialize/entity_paging
+      pager:
+        type: full
+        options:
+          items_per_page: 1
+          offset: 0
+          id: 0
+      style:
+        type: serializer
+        options:
+          uses_fields: false
+          formats:
+            hal_json: hal_json
diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_field.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_field.yml
index 2b981f0..c9e78d0 100644
--- a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_field.yml
+++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_display_field.yml
@@ -108,3 +108,31 @@ display:
         type: serializer
       row:
         type: data_field
+  rest_export_paging:
+    display_plugin: rest_export
+    id: rest_export_paging
+    display_title: 'serializer - paging'
+    position: null
+    display_options:
+      defaults:
+        access: false
+        row: false
+      style:
+        type: serializer
+        options:
+          uses_fields: false
+          formats:
+            hal_json: hal_json
+      path: test/serialize/field-paging
+      access:
+        type: none
+      style:
+        type: serializer
+      row:
+        type: data_field
+      pager:
+        type: full
+        options:
+          items_per_page: 1
+          offset: 0
+          id: 0
diff --git a/core/modules/serialization/src/Collection.php b/core/modules/serialization/src/Collection.php
new file mode 100644
index 0000000..c6f7301
--- /dev/null
+++ b/core/modules/serialization/src/Collection.php
@@ -0,0 +1,216 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\serialization\Collection
+ */
+
+namespace Drupal\serialization;
+
+/**
+ * Provides a wrapper for a collection of entities, e.g. a feed channel.
+ */
+class Collection implements \IteratorAggregate {
+
+  /**
+   * An internal identifier for this collection (e.g. view name).
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The items of the collection.
+   *
+   * @var array
+   */
+  protected $items = [];
+
+  /**
+   * The title of the collection.
+   *
+   * @var string
+   */
+  protected $title;
+
+  /**
+   * The URI of the collection.
+   *
+   * @var string
+   */
+  protected $uri;
+
+  /**
+   * The description of the collection.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * Hypermedia links (prev, next, first, last for paging collections)
+   *
+   * @var array
+   */
+  protected $links = [];
+
+  /**
+   * Constructor.
+   *
+   * @param string $collection_id
+   *   The internal identifier for the collection (e.g. view name).
+   */
+  public function __construct($collection_id) {
+    $this->id = $collection_id;
+  }
+
+  /**
+   * Get the internal ID of the collection.
+   */
+  public function getCollectionId() {
+    return $this->id;
+  }
+
+  /**
+   * Get the items in the collection.
+   *
+   * @return array
+   *   The items of the collection.
+   */
+  public function getItems() {
+    return $this->items;
+  }
+
+  /**
+   * Set the items list.
+   *
+   * @param array $items
+   *   The items of the collection.
+   */
+  public function setItems($items) {
+    $this->items = $items;
+  }
+
+  /**
+   * Get the collection title.
+   *
+   * @return string
+   *   The title of the collection.
+   */
+  public function getTitle() {
+    return $this->title;
+  }
+
+  /**
+   * Set the collection title.
+   *
+   * @param string $title
+   *   The items of the collection.
+   */
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+
+  /**
+   * Get the collection URI.
+   *
+   * @return string
+   *   The URI ("self") of the collection.
+   */
+  public function getUri() {
+    return $this->uri;
+  }
+
+  /**
+   * Set the collection URI.
+   *
+   * @param string $uri
+   *   The URI ("self") of the collection.
+   */
+  public function setUri($uri) {
+    $this->uri = $uri;
+  }
+
+  /**
+   * Get the collection description.
+   *
+   * @return string
+   *   The description of the collection.
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * Set the collection URI.
+   *
+   * @param string $description
+   *   The description of the collection.
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return new \ArrayIterator($this->getItems());
+  }
+
+  /**
+   * Sets a link URI for a given type.
+   *
+   * @param string $type
+   *   The link relation type.
+   * @param string $uri
+   *   The link relation URI.
+   */
+  public function setLink($type, $uri) {
+    $this->links[$type] = $uri;
+  }
+
+  /**
+   * Gets a link URI for a given type.
+   *
+   * @param string $type
+   *   The link relation type.
+   *
+   * @return NULL|String
+   *   The URI of the link relation type or NULL.
+   */
+  public function getLink($type) {
+    return isset($this->links[$type]) ? $this->links[$type] : NULL;
+  }
+
+  /**
+   * Sets all link URIs.
+   *
+   * @param array $links
+   *   Associative array of link relation types and URIs.
+   */
+  public function setLinks(array $links) {
+    $this->links = $links;
+  }
+
+  /**
+   * Gets all link URIs.
+   *
+   * @return array
+   *   Associative array of link relation types and URIs.
+   */
+  public function getLinks() {
+    return $this->links;
+  }
+
+  /**
+   * Returns true if (hypermedia) link relations have been added.
+   *
+   * @return bool
+   *   True if link relations have been added.
+   */
+  public function hasLinks() {
+    // $this->links should be an array, so no type checking is needed.
+    return (bool) count($this->links);
+  }
+}
diff --git a/core/modules/serialization/tests/src/Unit/CollectionTest.php b/core/modules/serialization/tests/src/Unit/CollectionTest.php
new file mode 100644
index 0000000..eadbce8
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/CollectionTest.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\serialization\Tests\CollectionTest.
+ */
+
+namespace Drupal\Tests\serialization\Unit;
+
+use Drupal\serialization\Collection;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the Collection class.
+ *
+ * @group Serialization
+ *
+ * @coversDefaultClass \Drupal\serialization\Collection
+ */
+class CollectionTest extends UnitTestCase {
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'CollectionTest',
+      'description' => 'Tests the Collection class used for serializing collections.',
+      'group' => 'Serialization',
+    );
+  }
+
+  /**
+   * Tests the constructor, as well as getters and setters.
+   *
+   * @covers ::__construct
+   * @covers ::getTitle
+   * @covers ::setTitle
+   * @covers ::getURI
+   * @covers ::setURI
+   * @covers ::getDescription
+   * @covers ::setDescription
+   */
+  public function testConstructor() {
+    $collection_id = $this->randomMachineName();
+    $collection = new Collection($collection_id);
+    $this->assertSame($collection_id, $collection->getCollectionId(), 'Id has been set accordingly');
+
+    $this->assertEquals($collection->getTitle(), NULL, 'Collection title is not set');
+    $collection->setTitle($collection_id);
+    $this->assertEquals($collection->getTitle(), $collection_id, 'Collection title has been set');
+
+    $this->assertEquals($collection->getUri(), NULL, 'Collection URI is not set');
+    $collection->setURI('http://example.com/' . $collection_id);
+    $this->assertEquals($collection->getUri(), 'http://example.com/' . $collection_id, 'Collection URI has been set');
+
+    $this->assertEquals($collection->getDescription(), NULL, 'Collection description is not set');
+    $collection->setDescription($collection_id);
+    $this->assertEquals($collection->getDescription(), $collection_id, 'Collection description has been set');
+  }
+
+  /**
+   * Tests items setter and getter as well as iteration.
+   *
+   * @covers ::setItems
+   * @covers ::getItems
+   * @covers ::getIterator
+   */
+  public function testSettingItemsAndIterating() {
+    $collection = new Collection('example');
+
+    $this->assertEquals($collection->getItems(), array(), 'By default the items are an empty array');
+
+    $example = array(1, 2, 3);
+    $collection->setItems($example);
+    $this->assertEquals($collection->getItems(), $example, 'Setters and getters work on the Collection');
+
+    // Iterating over collection returns the same array.
+    $array = array();
+    foreach ($collection as $k => $v) {
+      $array[$k] = $v;
+    }
+    $this->assertEquals($array, $collection->getItems(), 'Array filled via iteration matches the array of the getter');
+  }
+
+  /**
+   * Tests setters and getters for link relations.
+   *
+   * @covers ::setLinks
+   * @covers ::getLinks
+   * @covers ::hasLinks
+   * @covers ::getLink
+   * @covers ::setLink
+   */
+  public function testSetGetHasLinks() {
+    $collection = new Collection('example');
+    $this->assertFalse($collection->hasLinks(), 'By default a Collection does not have any link relations');
+
+    $collection->setURI('http://example.com/collection');
+    $this->assertFalse($collection->hasLinks(), 'Setting the main URI (self) of the collection does not count as a link relation');
+
+    $this->assertEquals($collection->getLink('next'), NULL, 'Before setting a link for a type the getter returns NULL');
+    $collection->setLink('next', 'http://example.com/collection/?page=1');
+    $this->assertEquals($collection->getLink('next'), 'http://example.com/collection/?page=1', 'After setting a link for a type the getter returns correctly');
+    $this->assertTrue($collection->hasLinks(), 'Setting another URI (self) of the collection does not count as a link relation');
+    $this->assertEquals($collection->getLinks(), array('next' => 'http://example.com/collection/?page=1'));
+
+    $collection->setLinks(array());
+    $this->assertFalse($collection->hasLinks(), 'After resetting links to an empty array, hasLinks should return false');
+  }
+}
