diff --git a/core/modules/hal/hal.info b/core/modules/hal/hal.info
new file mode 100644
index 0000000..ec43ea8
--- /dev/null
+++ b/core/modules/hal/hal.info
@@ -0,0 +1,6 @@
+name = HAL (Hypertext Application Language)
+description = Serializes entities using HAL.
+package = Core
+core = 8.x
+dependencies[] = rest
+dependencies[] = serialization
diff --git a/core/modules/hal/hal.module b/core/modules/hal/hal.module
new file mode 100644
index 0000000..72e9b86
--- /dev/null
+++ b/core/modules/hal/hal.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Drupal-required module file for HAL module.
+ */
diff --git a/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php b/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php
new file mode 100644
index 0000000..4d932ff
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\JsonEncoder.
+ */
+
+namespace Drupal\hal\Encoder;
+
+use Symfony\Component\Serializer\Encoder\JsonEncoder as SymfonyJsonEncoder;
+
+/**
+ * Encodes HAL data in JSON.
+ *
+ * Simply respond to application/hal+json requests using the JSON encoder.
+ */
+class JsonEncoder extends SymfonyJsonEncoder {
+
+  /**
+   * The formats that this Encoder supports.
+   *
+   * @var string
+   */
+  protected $format = 'hal_json';
+
+  /**
+   * Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsEncoding()
+   */
+  public function supportsEncoding($format) {
+    return $format == $this->format;
+  }
+
+  /**
+   * Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsDecoding()
+   */
+  public function supportsDecoding($format) {
+    return $format == $this->format;
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/HalBundle.php b/core/modules/hal/lib/Drupal/hal/HalBundle.php
new file mode 100644
index 0000000..d782512
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/HalBundle.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\HalBundle.
+ */
+
+namespace Drupal\hal;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * HAL dependency injection container.
+ */
+class HalBundle extends Bundle {
+
+  /**
+   * Overrides \Symfony\Component\HttpKernel\Bundle\Bundle::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $priority = 10;
+
+    $container->register('serializer.normalizer.entity_reference_item.hal', 'Drupal\hal\Normalizer\EntityReferenceItemNormalizer')
+      ->addTag('normalizer', array('priority' => $priority));
+    $container->register('serializer.normalizer.field_item.hal', 'Drupal\hal\Normalizer\FieldItemNormalizer')
+      ->addTag('normalizer', array('priority' => $priority));
+    $container->register('serializer.normalizer.field.hal', 'Drupal\hal\Normalizer\FieldNormalizer')
+      ->addTag('normalizer', array('priority' => $priority));
+    $container->register('serializer.normalizer.entity.hal', 'Drupal\hal\Normalizer\EntityNormalizer')
+      ->addMethodCall('setLinkManager', array(new Reference('rest.link_manager')))
+      ->addTag('normalizer', array('priority' => $priority));
+
+    $container->register('serializer.encoder.hal', 'Drupal\hal\Encoder\JsonEncoder')
+      ->addTag('encoder', array(
+        'priority' => $priority,
+        'format' => array(
+          'hal_json' => 'HAL (JSON)',
+        ),
+      ));
+
+    $container->register('hal.subscriber', 'Drupal\hal\HalSubscriber')
+      ->addTag('event_subscriber');
+  }
+}
diff --git a/core/modules/hal/lib/Drupal/hal/HalSubscriber.php b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php
new file mode 100644
index 0000000..93a70bb
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\HalSubscriber.
+ */
+
+namespace Drupal\hal;
+
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Subscribes to the kernel request event to add HAL media types.
+ */
+class HalSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Registers HAL formats with the Request class.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+   *   The event to process.
+   */
+  public function onKernelRequest(GetResponseEvent $event) {
+    $request = $event->getRequest();
+    $request->setFormat('hal_json', 'application/hal+json');
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::REQUEST][] = array('onKernelRequest', 40);
+    return $events;
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php
new file mode 100644
index 0000000..0fa3600
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\EntityNormalizer.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\EntityNG;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Converts the Drupal entity object structure to a HAL array structure.
+ */
+class EntityNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\EntityInterface';
+
+  /**
+   * The hypermedia link manager.
+   *
+   * @var \Drupal\rest\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
+   */
+  public function normalize($entity, $format = NULL, array $context = array()) {
+    // Create the array of normalized properties, starting with the URI.
+    $normalized = array(
+      '_links' => array(
+        'self' => array(
+          'href' => $this->getEntityUri($entity),
+        ),
+        'type' => array(
+          'href' => $this->linkManager->getTypeUri($entity->entityType(), $entity->bundle()),
+        ),
+      ),
+    );
+
+    // If the properties to use were specified, only output those properties.
+    // Otherwise, output all properties except internal ID.
+    if (isset($context['included_fields'])) {
+      foreach ($context['included_fields'] as $property_name) {
+        $properties[] = $entity->get($property_name);
+      }
+    }
+    else {
+      $properties = $entity->getProperties();
+    }
+    foreach ($properties as $property) {
+      // In some cases, Entity API will return NULL array items. Ensure this is
+      // a real property and that it is not the internal id.
+      if (!is_object($property) || $property->getName() == 'id') {
+        continue;
+      }
+      $normalized_property = $this->serializer->normalize($property, $format, $context);
+      $normalized = NestedArray::mergeDeep($normalized, $normalized_property);
+    }
+
+    // Only add the curies array if there are curies in the link relations.
+    $link_relations = array_keys($normalized['_links']);
+    if (isset($normalized['_embedded'])) {
+      $link_relations = array_merge($link_relations, array_keys($normalized['_embedded']));
+    }
+    foreach ($link_relations as $link_relation) {
+      if (strpos($link_relation, ':') == TRUE) {
+        $normalized['_links']['curies'][] = array(
+          // @todo Make this configurable.
+          'href' => url('relations') . '/{rel}',
+          'name' => 'site',
+          'templated' => TRUE,
+        );
+        break;
+      }
+    }
+
+    return $normalized;
+  }
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize().
+   *
+   * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+    // Get type, necessary for determining which bundle to create.
+    if (!isset($data['_links']['type'])) {
+      throw new UnexpectedValueException('The type link relation must be specified.');
+    }
+    $type_uri = $data['_links']['type']['href'];
+
+    // Get language. language_default() is used because otherwise the entity
+    // system cannot understand the langcode when working in the DrupalUnitTest
+    // environment and importing data without an explicit langcode.
+    $langcode = isset($data['langcode']) ? $data['langcode'][0]['value'] : language_default();
+
+    // Create the entity.
+    if ($typed_data_ids = $this->linkManager->getTypedDataIds($type_uri)) {
+      $entity = entity_create($typed_data_ids['entity_type'], array('langcode' => $langcode, 'type' => $typed_data_ids['bundle']));
+    }
+    else {
+      throw new UnexpectedValueException(sprintf('Type %s does not correspond to an entity on this site.', $type_uri));
+    }
+
+    // Get links and remove from data array.
+    $links = $data['_links'];
+    unset($data['_links']);
+    // Get embedded resources and remove from data array.
+    $embedded = array();
+    if (isset($data['_embedded'])) {
+      $embedded = $data['_embedded'];
+      unset($data['_embedded']);
+    }
+
+    // Iterate through remaining items in data array. These should all
+    // correspond to fields.
+    foreach ($data as $field_name => $field_data) {
+      // Remove any values that were set as a part of entity creation (e.g
+      // uuid). If this field is set to an empty array in the data, this will
+      // also have the effect of marking the field for deletion in REST module.
+      $entity->{$field_name} = array();
+
+      // Get the class of the field. This will generally be the default Field
+      // class.
+      $field = $entity->get($field_name);
+      $class = get_class($field);
+      $context['target_instance'] = $field;
+      $field = $this->serializer->denormalize($field_data, $class, $format, $context);
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Constructs the entity URI.
+   *
+   * @param $entity
+   *   The entity.
+   *
+   * @return string
+   *   The entity URI.
+   */
+  protected function getEntityUri($entity) {
+    // @todo Remove this conditional once entities are converted to EntityNG.
+    if ($entity instanceof EntityNG) {
+      $uri_info = $entity->uri();
+      return url($uri_info['path']);
+    }
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php
new file mode 100644
index 0000000..0142d02
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\EntityReferenceItemNormalizer.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+/**
+ * Converts the Drupal entity reference item object to HAL array structure.
+ */
+class EntityReferenceItemNormalizer extends FieldItemNormalizer {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\Type\EntityReferenceItem';
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
+   */
+  public function normalize($field_item, $format = NULL, array $context = array()) {
+    $target_entity = $field_item->get('entity')->getValue();
+
+    // If the parent entity passed in a langcode, unset it before normalizing
+    // the target entity. Otherwise, untranslatable fields of the target entity
+    // will include the langcode.
+    $langcode = isset($context['langcode']) ? $context['langcode'] : NULL;
+    unset($context['langcode']);
+    $context['included_fields'] = array('uuid');
+
+    // Normalize the target entity.
+    $embedded = $this->serializer->normalize($target_entity, $format, $context);
+    $link = $embedded['_links']['self'];
+    // If the field is translatable, add the langcode to the link relation
+    // object. This does not indicate the language of the target entity.
+    if ($langcode) {
+      $embedded['lang'] = $link['lang'] = $langcode;
+    }
+
+    // The returned structure will be recursively merged into the normalized
+    // entity so that the items are properly added to the _links and _embedded
+    // objects.
+    $field_name = $field_item->getParent()->getName();
+    // @todo Introduce a RelationLinkManager to get the CURIE.
+    $field_curie = "site:$field_name";
+    return array(
+      '_links' => array(
+        $field_curie => array($link),
+      ),
+      '_embedded' => array(
+        $field_curie => array($embedded),
+      ),
+    );
+  }
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php
new file mode 100644
index 0000000..9b4ca14
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\FieldItemNormalizer.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\Core\Entity\Field\FieldItemInterface;
+
+/**
+ * Converts the Drupal field item object structure to HAL array structure.
+ */
+class FieldItemNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\FieldItemInterface';
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
+   */
+  public function normalize($field_item, $format = NULL, array $context = array()) {
+    $values = $field_item->getPropertyValues();
+    if (isset($context['langcode'])) {
+      $values['lang'] = $context['langcode'];
+    }
+
+    // The values are wrapped in an array, and then wrapped in another array
+    // keyed by field name so that field items can be merged by the
+    // FieldNormalizer. This is necessary for the EntityReferenceItemNormalizer
+    // to be able to place values in the '_links' array.
+    $field = $field_item->getParent();
+    return array(
+      $field->getName() => array($values),
+    );
+  }
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+    if (!isset($context['target_instance'])) {
+      throw new LogicException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
+    }
+    if ($context['target_instance']->getParent() == NULL) {
+      throw new LogicException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
+    }
+
+    $field_item = $context['target_instance'];
+
+    // If this field is translatable, we need to create a translated instance.
+    if (isset($data['lang'])) {
+      $langcode = $data['lang'];
+      unset($data['lang']);
+      $field_definition = $field_item->getDefinition();
+      if ($field_definition['translatable'] == TRUE) {
+        $field_item = $this->createTranslatedInstance($field_item, $langcode);
+      }
+    }
+
+    $field_item->setValue($data);
+    return $field_item;
+  }
+
+  /**
+   * Get a translated version of the field item instance.
+   *
+   * @param \Drupal\Core\Entity\Field\FieldItemInterface $field_item
+   *   The untranslated field item instance.
+   * @param $langcode
+   *   The langcode.
+   *
+   * @return \Drupal\Core\Entity\Field\FieldItemInterface
+   *   The translated field item instance.
+   */
+  protected function createTranslatedInstance(FieldItemInterface $field_item, $langcode) {
+    $parent = $field_item->getParent();
+    $ancestors = array();
+
+    // Remove the untranslated instance from the field's list of items.
+    $parent->offsetUnset($field_item->getName());
+
+    // Get the property chain.
+    while (!method_exists($parent, 'getTranslation')) {
+      array_unshift($ancestors, $parent);
+      $parent = $parent->getParent();
+    }
+
+    // Recreate the property path.
+    $translation = $parent->getTranslation($langcode);
+    foreach ($ancestors as $ancestor) {
+      $ancestor_name =  $ancestor->getName();
+      $translation = $translation->get($ancestor_name);
+    }
+
+    // Create a new instance at the end of the property path and return it.
+    $count = $translation->isEmpty() ? 0 : $translation->count();
+    return $translation->offsetGet($count);
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php
new file mode 100644
index 0000000..ae48b57
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\FieldNormalizer.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\Component\Utility\NestedArray;
+use Symfony\Component\Serializer\Exception\LogicException;
+
+/**
+ * Converts the Drupal field structure to HAL array structure.
+ */
+class FieldNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\FieldInterface';
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
+   */
+  public function normalize($field, $format = NULL, array $context = array()) {
+    $normalized_field_items = array();
+    $entity = $field->getParent();
+    $field_name = $field->getName();
+    $field_definition = $entity->getPropertyDefinition($field_name);
+
+    // If this field is not translatable, it can simply be normalized without
+    // separating it into different translations.
+    if (empty($field_definition['translatable'])) {
+      $normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
+    }
+    // Otherwise, the languages have to be extracted from the entity and passed
+    // in to the field item normalizer in the context. The langcode is appended
+    // to the field item values.
+    else {
+      foreach ($entity->getTranslationLanguages() as $lang) {
+        $context['langcode'] = $lang->langcode == 'und' ? LANGUAGE_DEFAULT : $lang->langcode;
+        $translation = $entity->getTranslation($lang->langcode);
+        $translated_field = $translation->get($field_name);
+        $normalized_field_items = array_merge($normalized_field_items, $this->normalizeFieldItems($translated_field, $format, $context));
+      }
+    }
+
+    // Merge deep so that links set in entity reference normalizers are merged
+    // into the links property.
+    $normalized = NestedArray::mergeDeepArray($normalized_field_items);
+    return $normalized;
+  }
+
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+    if (!isset($context['target_instance'])) {
+      throw new LogicException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
+    }
+    if ($context['target_instance']->getParent() == NULL) {
+      throw new LogicException('The field passed in via $context[\'target_instance\'] must have a parent set.');
+    }
+
+    $field = $context['target_instance'];
+    foreach ($data as $field_item_data) {
+      $count = $field->isEmpty() ? 0 : $field->count();
+      $field_item = $field->offsetGet($count);
+      $field_item_class = get_class($field_item);
+      $context['target_instance'] = $field_item;
+      $this->serializer->denormalize($field_item_data, $field_item_class, $format, $context);
+    }
+
+    return $field;
+
+  }
+
+  /**
+   * Helper function to normalize field items.
+   *
+   * @param \Drupal\Core\Entity\Field\FieldInterface $field
+   *   The field object.
+   * @param string $format
+   *   The format.
+   * @param array $context
+   *   The context array.
+   *
+   * @return array
+   *   The array of normalized field items.
+   */
+  protected function normalizeFieldItems($field, $format, $context) {
+    $normalized_field_items = array();
+    if (!$field->isEmpty()) {
+      foreach ($field as $field_item) {
+        $normalized_field_items[] = $this->serializer->normalize($field_item, $format, $context);
+      }
+    }
+    return $normalized_field_items;
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php
new file mode 100644
index 0000000..333099c
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Normalizer\NormalizerBase.
+ */
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Base class for Normalizers.
+ */
+abstract class NormalizerBase extends SerializationNormalizerBase implements DenormalizerInterface {
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = array('hal_json');
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization().
+   */
+  public function supportsNormalization($data, $format = NULL) {
+    return in_array($format, $this->formats) && parent::supportsNormalization($data, $format);
+  }
+
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization()
+   */
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    if (in_array($format, $this->formats)) {
+      $target = new \ReflectionClass($type);
+      $supported = new \ReflectionClass($this->supportedInterfaceOrClass);
+      if ($supported->isInterface()) {
+        return $target->implementsInterface($this->supportedInterfaceOrClass);
+      }
+      else {
+        return ($target->getName() == $this->supportedInterfaceOrClass || $target->isSubclassOf($this->supportedInterfaceOrClass));
+      }
+    }
+  }
+
+  /**
+   * Sets the link manager.
+   *
+   * The link manager determines the hypermedia type and relation links which
+   * correspond to different bundles and fields.
+   *
+   * @param \Drupal\rest\LinkManager\LinkManager $link_manager
+   */
+  public function setLinkManager($link_manager) {
+    $this->linkManager = $link_manager;
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php b/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php
new file mode 100644
index 0000000..9ff32e7
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Tests\DenormalizeTest.
+ */
+
+namespace Drupal\hal\Tests;
+
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Test the HAL normalizer's denormalize function.
+ */
+class DenormalizeTest extends NormalizerTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Denormalize Test',
+      'description' => 'Test that entities can be denormalized from HAL.',
+      'group' => 'HAL',
+    );
+  }
+
+  /**
+   * Tests that the type link relation in incoming data is handled correctly.
+   */
+  public function testTypeHandling() {
+    // Valid type.
+    $data_with_valid_type = array(
+      '_links' => array(
+        'type' => array(
+          'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+        ),
+      ),
+    );
+    $denormalized = $this->container->get('serializer')->denormalize($data_with_valid_type, $this->entityClass, $this->format);
+    $this->assertEqual(get_class($denormalized),   $this->entityClass, 'Request with valid type results in creation of correct bundle.');
+
+    // Invalid type.
+    $data_with_invalid_type = array(
+      '_links' => array(
+        'type' => array(
+          'href' => url('rest/types/foo', array('absolute' => TRUE)),
+        ),
+      ),
+    );
+    try {
+      $this->container->get('serializer')->denormalize($data_with_invalid_type,   $this->entityClass, $this->format);
+      $this->fail('Exception thrown when type is invalid.');
+    }
+    catch (UnexpectedValueException $e) {
+      $this->pass('Exception thrown when type is invalid.');
+    }
+
+    // No type.
+    $data_with_no_type = array(
+      '_links' => array(
+      ),
+    );
+    try {
+      $this->container->get('serializer')->denormalize($data_with_no_type,   $this->entityClass, $this->format);
+      $this->fail('Exception thrown when no type is provided.');
+    }
+    catch (UnexpectedValueException $e) {
+      $this->pass('Exception thrown when no type is provided.');
+    }
+  }
+
+  /**
+   * Test that a field set to an empty array is different than an empty field.
+   */
+  public function testMarkFieldForDeletion() {
+    $no_field_data = array(
+      '_links' => array(
+        'type' => array(
+          'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+        ),
+      ),
+    );
+    $no_field_denormalized = $this->container->get('serializer')->denormalize($no_field_data, $this->entityClass, $this->format);
+    $no_field_value = $no_field_denormalized->field_test_text->getValue();
+
+    $empty_field_data = array(
+      '_links' => array(
+        'type' => array(
+          'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+        ),
+      ),
+      'field_test_text' => array(),
+    );
+    $empty_field_denormalized = $this->container->get('serializer')->denormalize($empty_field_data, $this->entityClass, $this->format);
+    $empty_field_value = $empty_field_denormalized->field_test_text->getValue();
+
+    $this->assertTrue(!empty($no_field_value) && empty($empty_field_value), 'A field set to an empty array in the data is structured differently than an empty field.');
+  }
+
+  /**
+   * Test that non-reference fields can be denormalized.
+   */
+  public function testBasicFieldDenormalization() {
+    $data = array(
+      '_links' => array(
+        'type' => array(
+          'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+        ),
+      ),
+      'uuid' => array(
+        array(
+          'value' => 'e5c9fb96-3acf-4a8d-9417-23de1b6c3311',
+        ),
+      ),
+      'field_test_text' => array(
+        array(
+          'value' => $this->randomName(),
+          'format' => 'full_html',
+        ),
+      ),
+      'field_test_translatable_text' => array(
+        array(
+          'value' => $this->randomName(),
+          'format' => 'full_html',
+          'lang' => 'en',
+        ),
+        array(
+          'value' => $this->randomName(),
+          'format' => 'filtered_html',
+          'lang' => 'en',
+        ),
+        array(
+          'value' => $this->randomName(),
+          'format' => 'filtered_html',
+          'lang' => 'de',
+        ),
+        array(
+          'value' => $this->randomName(),
+          'format' => 'full_html',
+          'lang' => 'de',
+        ),
+      ),
+    );
+
+    $expected_value_en = array(
+      array (
+        'value' => $data['field_test_translatable_text'][0]['value'],
+        'format' => 'full_html',
+      ),
+      array (
+        'value' => $data['field_test_translatable_text'][1]['value'],
+        'format' => 'filtered_html',
+      ),
+    );
+    $expected_value_de = array(
+      array (
+        'value' => $data['field_test_translatable_text'][2]['value'],
+        'format' => 'filtered_html',
+      ),
+      array (
+        'value' => $data['field_test_translatable_text'][3]['value'],
+        'format' => 'full_html',
+      ),
+    );
+    $denormalized = $this->container->get('serializer')->denormalize($data, $this->entityClass, $this->format);
+    $this->assertEqual($data['uuid'], $denormalized->get('uuid')->getValue(), 'A preset value (e.g. UUID) is overridden by incoming data.');
+    $this->assertEqual($data['field_test_text'], $denormalized->get('field_test_text')->getValue(), 'A basic text field is denormalized.');
+    $this->assertEqual($expected_value_en, $denormalized->get('field_test_translatable_text')->getValue(), 'Values in the default language are properly handled for a translatable field.');
+    $this->assertEqual($expected_value_de, $denormalized->getTranslation('de')->get('field_test_translatable_text')->getValue(), 'Values in a translation language are properly handled for a translatable field.');
+  }
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php
new file mode 100644
index 0000000..c87941e
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Tests\NormalizeTest.
+ */
+
+namespace Drupal\hal\Tests;
+
+/**
+ * Test the HAL normalizer.
+ */
+class NormalizeTest extends NormalizerTestBase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Normalize Test',
+      'description' => 'Test that entities can be normalized in HAL.',
+      'group' => 'HAL',
+    );
+  }
+
+  /**
+   * Tests the normalize function.
+   */
+  public function testNormalize() {
+    $target_entity_de = entity_create('entity_test', (array('langcode' => 'de', 'field_test_entity_reference' => NULL)));
+    $target_entity_de->save();
+    $target_entity_en = entity_create('entity_test', (array('langcode' => 'en', 'field_test_entity_reference' => NULL)));
+    $target_entity_en->save();
+
+    // Create a German entity.
+    $values = array(
+      'langcode' => 'de',
+      'name' => $this->randomName(),
+      'user_id' => 1,
+      'field_test_text' => array(
+        'value' => $this->randomName(),
+        'format' => 'full_html',
+      ),
+      'field_test_entity_reference' => array(
+        'target_id' => $target_entity_de->id(),
+      ),
+    );
+    // Array of translated values.
+    $translation_values = array(
+      'name' => $this->randomName(),
+      'field_test_entity_reference' => array(
+        'target_id' => $target_entity_en->id(),
+      )
+    );
+
+    $entity = entity_create('entity_test', $values);
+    $entity->save();
+    // Add an English value for name and entity reference properties.
+    $entity->getTranslation('en')->set('name', array(0 => array('value' => $translation_values['name'])));
+    $entity->getTranslation('en')->set('field_test_entity_reference', array(0 => $translation_values['field_test_entity_reference']));
+    $entity->save();
+
+    $expected_array = array(
+      '_links' => array(
+        'curies' => array(
+          array(
+            'href' => '/relations',
+            'name' => 'site',
+            'templated' => true,
+          ),
+        ),
+        'self' => array(
+          'href' => $this->getUri($entity),
+        ),
+        'type' => array(
+          'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+        ),
+        'site:user_id' => array(
+          array(
+            'href' => NULL,
+            'lang' => 'de',
+          ),
+        ),
+        'site:field_test_entity_reference' => array(
+          array(
+            'href' => $this->getUri($target_entity_de),
+            'lang' => 'de',
+          ),
+          array(
+            'href' => $this->getUri($target_entity_en),
+            'lang' => 'en',
+          ),
+        ),
+      ),
+      '_embedded' => array(
+        'site:user_id' => array(
+          array(
+            'href' => NULL,
+            'lang' => 'de',
+          ),
+        ),
+        'site:field_test_entity_reference' => array(
+          array(
+            '_links' => array(
+              'self' => array(
+                'href' => $this->getUri($target_entity_de),
+              ),
+              'type' => array(
+                'href' =>url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+              ),
+            ),
+            'uuid' => array(
+              array(
+                'value' => $target_entity_de->uuid(),
+              ),
+            ),
+            'lang' => 'de',
+          ),
+          array(
+            '_links' => array(
+              'self' => array(
+                'href' => $this->getUri($target_entity_en),
+              ),
+              'type' => array(
+                'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)),
+              ),
+            ),
+            'uuid' => array(
+              array(
+                'value' => $target_entity_en->uuid(),
+              ),
+            ),
+            'lang' => 'en',
+          ),
+        ),
+      ),
+      'uuid' => array(
+        array(
+          'value' => $entity->uuid(),
+        ),
+      ),
+      'langcode' => array(
+        array(
+          'value' => 'de',
+        ),
+      ),
+      'name' => array(
+        array(
+          'value' => $values['name'],
+          'lang' => 'de',
+        ),
+        array(
+          'value' => $translation_values['name'],
+          'lang' => 'en',
+        ),
+      ),
+      'field_test_text' => array(
+        array(
+          'value' => $values['field_test_text']['value'],
+          'format' => $values['field_test_text']['format'],
+        ),
+      ),
+    );
+
+    $normalized = $this->container->get('serializer')->normalize($entity, $this->format);
+    $this->assertEqual($normalized['_links']['self'], $expected_array['_links']['self'], 'self link placed correctly.');
+    // @todo Test curies.
+    // @todo Test type.
+    $this->assertFalse(isset($normalized['id']), 'Internal id is not exposed.');
+    $this->assertEqual($normalized['uuid'], $expected_array['uuid'], 'Non-translatable fields is normalized.');
+    $this->assertEqual($normalized['name'], $expected_array['name'], 'Translatable field with multiple language values is normalized.');
+    $this->assertEqual($normalized['field_test_text'], $expected_array['field_test_text'], 'Field with properties is normalized.');
+    $this->assertEqual($normalized['_embedded']['site:field_test_entity_reference'], $expected_array['_embedded']['site:field_test_entity_reference'], 'Entity reference field is normalized.');
+    $this->assertEqual($normalized['_links']['site:field_test_entity_reference'], $expected_array['_links']['site:field_test_entity_reference'], 'Links are added for entity reference field.');
+  }
+
+  protected function getUri($entity) {
+    $entity_uri_info = $entity->uri();
+    return url($entity_uri_info['path']);
+  }
+
+}
diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php
new file mode 100644
index 0000000..385b5c3
--- /dev/null
+++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\hal\Tests\NormalizerTestBase.
+ */
+
+namespace Drupal\hal\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+
+/**
+ * Test the HAL normalizer.
+ */
+abstract class NormalizerTestBase extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_test', 'entity_reference', 'field', 'field_sql_storage', 'hal', 'language', 'rest', 'serialization', 'system', 'text', 'user');
+
+  /**
+   * The format being tested.
+   *
+   * @var string
+   */
+  protected $format = 'hal_json';
+
+  /**
+   * The class name of the test class.
+   *
+   * @var string
+   */
+  protected $entityClass = 'Drupal\entity_test\Plugin\Core\Entity\EntityTest';
+
+  /**
+   * Overrides \Drupal\simpletest\DrupalUnitTestBase::setup().
+   */
+  function setUp() {
+    parent::setUp();
+    $this->installSchema('system', array('variable', 'url_alias'));
+    $this->installSchema('field', array('field_config', 'field_config_instance'));
+    $this->installSchema('user', array('users'));
+    $this->installSchema('language', array('language'));
+    $this->installSchema('entity_test', array('entity_test'));
+
+    // Add English as a language.
+    $english = new Language(array(
+      'langcode' => 'en',
+      'name' => 'English',
+    ));
+    language_save($english);
+    // Add German as a language.
+    $german = new Language(array(
+      'langcode' => 'de',
+      'name' => 'Deutsch',
+    ));
+    language_save($german);
+
+    // Create the test text field.
+    $field = array(
+      'field_name' => 'field_test_text',
+      'type' => 'text',
+      'translatable' => FALSE,
+    );
+    field_create_field($field);
+    $instance = array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test_text',
+      'bundle' => 'entity_test',
+    );
+    field_create_instance($instance);
+
+    // Create the test translatable field.
+    $field = array(
+      'field_name' => 'field_test_translatable_text',
+      'type' => 'text',
+      'translatable' => TRUE,
+    );
+    field_create_field($field);
+    $instance = array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test_translatable_text',
+      'bundle' => 'entity_test',
+    );
+    field_create_instance($instance);
+
+    // Create the test entity reference field.
+    $field = array(
+      'translatable' => TRUE,
+      'settings' => array(
+        'target_type' => 'entity_test',
+      ),
+      'field_name' => 'field_test_entity_reference',
+      'type' => 'entity_reference',
+    );
+    field_create_field($field);
+    $instance = array(
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test_entity_reference',
+      'bundle' => 'entity_test',
+    );
+    field_create_instance($instance);
+  }
+
+}
diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php
new file mode 100644
index 0000000..5817969
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\LinkManager.
+ */
+
+namespace Drupal\rest\LinkManager;
+
+class LinkManager implements LinkManagerInterface {
+
+  /**
+   * The type link manager.
+   *
+   * @var \Drupal\rest\LinkManager\TypeLinkManagerInterface
+   */
+  protected $typeLinkManager;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_link_manager
+   */
+  public function __construct(TypeLinkManagerInterface $type_link_manager) {
+    $this->typeLinkManager = $type_link_manager;
+  }
+
+  /**
+   * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeUri().
+   */
+  public function getTypeUri($entity_type, $bundle) {
+    return $this->typeLinkManager->getTypeUri($entity_type, $bundle);
+  }
+
+  /**
+   * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypedDataIds().
+   */
+  public function getTypedDataIds($type_uri) {
+    return $this->typeLinkManager->getTypedDataIds($type_uri);
+  }
+}
diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php
new file mode 100644
index 0000000..30f5d25
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\LinkManagerInterface
+ */
+
+namespace Drupal\rest\LinkManager;
+
+/**
+ * Interface implemented by link managers.
+ *
+ * There are no explicit methods on the manager interface. Instead link managers
+ * broker the interactions of the different components, and therefore must
+ * implement each component interface, which is enforced by this interface
+ * extending all of the component ones.
+ *
+ * While a link manager may directly implement these interface methods with
+ * 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 {
+}
diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php
new file mode 100644
index 0000000..0349127
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\TypeLinkManager.
+ */
+
+namespace Drupal\rest\LinkManager;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+
+class TypeLinkManager implements TypeLinkManagerInterface {
+
+  /**
+   * Injected cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface;
+   */
+  protected $cache;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The injected cache backend for caching type URIs.
+   */
+  public function __construct(CacheBackendInterface $cache) {
+    $this->cache = $cache;
+  }
+
+  /**
+   * Get a type link for a bundle.
+   *
+   * @param string $entity_type
+   *   The bundle's entity type.
+   * @param string $bundle
+   *   The name of the bundle.
+   *
+   * @return array
+   *   The URI that identifies this bundle.
+   */
+  public function getTypeUri($entity_type, $bundle) {
+    // @todo Make the base path configurable.
+    return url("rest/types/$entity_type/$bundle", array('absolute' => TRUE));
+  }
+
+  /**
+   * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypedDataIds().
+   */
+  public function getTypedDataIds($type_uri) {
+    $types = $this->getTypes();
+    if (isset($types[$type_uri])) {
+      return $types[$type_uri];
+    }
+    return FALSE;
+  }
+
+  /**
+   * Get the array of type links.
+   *
+   * @return array
+   *   An array of typed data ids (entity_type and bundle) keyed by
+   *   corresponding type URI.
+   */
+  public function getTypes() {
+    $cid = 'rest:links:types';
+    $cache = $this->cache->get($cid);
+    if (!$cache) {
+      $this->writeCache();
+      $cache = $this->cache->get($cid);
+    }
+    return $cache->data;
+  }
+
+  /**
+   * Writes the cache of type links.
+   */
+  protected function writeCache() {
+    $data = array();
+
+    // Type URIs correspond to bundles. Iterate through the bundles to get the
+    // URI and data for them.
+    $entity_info = entity_get_info();
+    foreach (entity_get_bundles() as $entity_type => $bundles) {
+      $entity_type_info = $entity_info[$entity_type];
+      $reflection = new \ReflectionClass($entity_type_info['class']);
+      // Only content entities are supported currently.
+      // @todo Consider supporting config entities.
+      if ($reflection->implementsInterface('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
+        continue;
+      }
+      foreach ($bundles as $bundle => $bundle_info) {
+        // Get a type URI for the bundle.
+        $bundle_uri = $this->getTypeUri($entity_type, $bundle);
+        $data[$bundle_uri] = array(
+          'entity_type' => $entity_type,
+          'bundle' => $bundle,
+        );
+      }
+    }
+    // These URIs only change when entity info changes, so cache it permanently
+    // and only clear it when entity_info is cleared.
+    $this->cache->set('rest:links:types', $data, CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE));
+  }
+}
diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php
new file mode 100644
index 0000000..c07cece
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\rest\LinkManager\TypeLinkManagerInterface.
+ */
+
+namespace Drupal\rest\LinkManager;
+
+interface TypeLinkManagerInterface {
+
+  /**
+   * Gets the URI that corresponds to a bundle.
+   *
+   * When using hypermedia formats, this URI can be used to indicate which
+   * bundle the data represents. Documentation about required and optional
+   * fields can also be provided at this URI.
+   *
+   * @param $entity_type
+   *   The bundle's entity type.
+   * @param $bundle
+   *   The bundle name.
+   *
+   * @return string
+   *   The corresponding URI for the bundle.
+   */
+  public function getTypeUri($entity_type, $bundle);
+
+  /**
+   * Get a bundle's Typed Data IDs based on a URI.
+   *
+   * @param string $type_uri
+   *   The type URI.
+   *
+   * @return array | boolean
+   *   If the URI matches a bundle, returns an array containing entity_type and
+   *   bundle. Otherwise, returns false.
+   */
+  public function getTypedDataIds($type_uri);
+}
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
index 541c80b..61d05a5 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
@@ -155,6 +155,11 @@ public function patch($id, EntityInterface $entity) {
     }
     // Overwrite the received properties.
     foreach ($entity->getProperties() as $name => $property) {
+      // Requests cannot overwrite id or uuid, so skip them.
+      if (in_array($name, array('id', 'uuid'))) {
+        continue;
+      }
+
       if (isset($entity->{$name})) {
         $original_entity->{$name} = $property;
       }
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index 30156de..e0d09bc 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -75,7 +75,7 @@ public function handle(Request $request, $id = NULL) {
     }
     catch (HttpException $e) {
       $error['error'] = $e->getMessage();
-      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
+      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_json';
       $content = $serializer->serialize($error, $format);
       // Add the default content type, but only if the headers from the
       // exception have not specified it already.
@@ -88,9 +88,8 @@ public function handle(Request $request, $id = NULL) {
     if ($data != NULL) {
       // All REST routes are restricted to exactly one format, so instead of
       // parsing it out of the Accept headers again we can simply retrieve the
-      // format requirement. If there is no format associated just pick Drupal
-      // JSON-LD.
-      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld';
+      // format requirement. If there is no format associated just pick HAL.
+      $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_json';
 
       $output = $serializer->serialize($data, $format);
       $response->setContent($output);
diff --git a/core/modules/rest/lib/Drupal/rest/RestBundle.php b/core/modules/rest/lib/Drupal/rest/RestBundle.php
index 49a1169..36e2524 100644
--- a/core/modules/rest/lib/Drupal/rest/RestBundle.php
+++ b/core/modules/rest/lib/Drupal/rest/RestBundle.php
@@ -32,5 +32,10 @@ public function build(ContainerBuilder $container) {
 
     $container->register('access_check.rest.csrf', 'Drupal\rest\Access\CSRFAccessCheck')
       ->addTag('access_check');
+
+    $container->register('rest.link_manager', 'Drupal\rest\LinkManager\LinkManager')
+      ->addArgument(new Reference('rest.link_manager.type'));
+    $container->register('rest.link_manager.type', 'Drupal\rest\LinkManager\TypeLinkManager')
+      ->addArgument(new Reference('cache.cache'));
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
index a3a5c60..3420652 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
@@ -19,7 +19,7 @@ class CreateTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('rest', 'entity_test');
+  public static $modules = array('hal', 'rest', 'entity_test');
 
   public static function getInfo() {
     return array(
@@ -46,9 +46,9 @@ public function testCreate() {
 
     $entity_values = $this->entityValues($entity_type);
     $entity = entity_create($entity_type, $entity_values);
-    $serialized = $serializer->serialize($entity, 'drupal_jsonld');
+    $serialized = $serializer->serialize($entity, 'hal_json');
     // Create the entity over the web API.
-    $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/hal+json');
     $this->assertResponse(201);
 
     // Get the new entity ID from the location header and try to read it from
@@ -71,7 +71,7 @@ public function testCreate() {
     $loaded_entity->delete();
 
     // Try to send invalid data that cannot be correctly deserialized.
-    $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', 'application/hal+json');
     $this->assertResponse(400);
 
     // Try to create an entity without the CSRF token.
@@ -82,21 +82,21 @@ public function testCreate() {
       CURLOPT_POSTFIELDS => $serialized,
       CURLOPT_URL => url('entity/' . $entity_type, array('absolute' => TRUE)),
       CURLOPT_NOBODY => FALSE,
-      CURLOPT_HTTPHEADER => array('Content-Type: application/vnd.drupal.ld+json'),
+      CURLOPT_HTTPHEADER => array('Content-Type: application/hal+json'),
     ));
     $this->assertResponse(403);
     $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
 
     // Try to create an entity without proper permissions.
     $this->drupalLogout();
-    $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/hal+json');
     $this->assertResponse(403);
     $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
 
     // Try to create a resource which is not web API enabled.
     $this->enableService(FALSE);
     $this->drupalLogin($account);
-    $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/hal+json');
     $this->assertResponse(404);
     $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.');
 
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
index d4b9cc2..f93820c 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
@@ -19,7 +19,7 @@ class DBLogTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('jsonld', 'rest', 'dblog');
+  public static $modules = array('hal', 'rest', 'dblog');
 
   public static function getInfo() {
     return array(
@@ -50,16 +50,16 @@ public function testWatchdog() {
     $account = $this->drupalCreateUser(array('restful get dblog'));
     $this->drupalLogin($account);
 
-    $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/vnd.drupal.ld+json');
+    $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/hal+json');
     $this->assertResponse(200);
-    $this->assertHeader('content-type', 'application/vnd.drupal.ld+json');
+    $this->assertHeader('content-type', 'application/hal+json');
     $log = drupal_json_decode($response);
     $this->assertEqual($log['wid'], $id, 'Log ID is correct.');
     $this->assertEqual($log['type'], 'rest_test', 'Type of log message is correct.');
     $this->assertEqual($log['message'], 'Test message', 'Log message text is correct.');
 
     // Request an unknown log entry.
-    $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/vnd.drupal.ld+json');
+    $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/hal+json');
     $this->assertResponse(404);
     $decoded = drupal_json_decode($response);
     $this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.');
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index c29e7e6..8259db8 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -19,7 +19,7 @@ class DeleteTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('rest', 'entity_test');
+  public static $modules = array('hal', 'rest', 'entity_test');
 
   public static function getInfo() {
     return array(
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index c428c86..f69eb81 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -26,7 +26,7 @@
    * @param string $format
    *   The MIME type of the transmitted content.
    */
-  protected function httpRequest($url, $method, $body = NULL, $format = 'application/ld+json') {
+  protected function httpRequest($url, $method, $body = NULL, $format = 'application/hal+json') {
     if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
       // GET the CSRF token first for writing requests.
       $token = $this->drupalGet('rest/session/token');
@@ -159,7 +159,7 @@ protected function entityValues($entity_type) {
    * @param string $method
    *   The HTTP method to enable, e.g. GET, POST etc.
    * @param string $format
-   *   (Optional) The serialization format, e.g. jsonld.
+   *   (Optional) The serialization format, e.g. hal_json.
    */
   protected function enableService($resource_type, $method = 'GET', $format = NULL) {
     // Enable web API for this entity type.
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index 299bdd4..87c4aaa 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -19,7 +19,7 @@ class ReadTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('jsonld', 'rest', 'entity_test');
+  public static $modules = array('hal', 'rest', 'entity_test');
 
   public static function getInfo() {
     return array(
@@ -48,34 +48,34 @@ public function testRead() {
       $entity = $this->entityCreate($entity_type);
       $entity->save();
       // Read it over the web API.
-      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
+      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/hal+json');
       $this->assertResponse('200', 'HTTP response code is correct.');
-      $this->assertHeader('content-type', 'application/vnd.drupal.ld+json');
+      $this->assertHeader('content-type', 'application/hal+json');
       $data = drupal_json_decode($response);
       // Only assert one example property here, other properties should be
       // checked in serialization tests.
-      $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
+      $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
 
       // Try to read the entity with an unsupported mime format.
       $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
       $this->assertResponse(415);
 
       // Try to read an entity that does not exist.
-      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/vnd.drupal.ld+json');
+      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/hal+json');
       $this->assertResponse(404);
       $decoded = drupal_json_decode($response);
       $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
 
       // Try to read an entity without proper permissions.
       $this->drupalLogout();
-      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
+      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/hal+json');
       $this->assertResponse(403);
       $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
     }
     // Try to read a resource which is not web API enabled.
     $account = $this->drupalCreateUser();
     $this->drupalLogin($account);
-    $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
+    $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/hal+json');
     $this->assertResponse(404);
     $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
   }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
index 94be2bf..87f7e71 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php
@@ -19,7 +19,7 @@ class UpdateTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('rest', 'entity_test');
+  public static $modules = array('hal', 'rest', 'entity_test');
 
   public static function getInfo() {
     return array(
@@ -51,12 +51,10 @@ public function testPatchUpdate() {
     // Create a second stub entity for overwriting a field.
     $patch_values['field_test_text'] = array(0 => array('value' => $this->randomString()));
     $patch_entity = entity_create($entity_type, $patch_values);
-    // We don't want to overwrite the UUID.
-    unset($patch_entity->uuid);
-    $serialized = $serializer->serialize($patch_entity, 'drupal_jsonld');
+    $serialized = $serializer->serialize($patch_entity, 'hal_json');
 
     // Update the entity over the web API.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json');
     $this->assertResponse(204);
 
     // Re-load updated entity from the database.
@@ -64,12 +62,12 @@ public function testPatchUpdate() {
     $this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field was successfully updated.');
 
     // Try to empty a field.
-    $normalized = $serializer->normalize($patch_entity, 'drupal_jsonld');
+    $normalized = $serializer->normalize($patch_entity, 'hal_json');
     $normalized['field_test_text'] = array();
-    $serialized = $serializer->encode($normalized, 'jsonld');
+    $serialized = $serializer->encode($normalized, 'hal_json');
 
     // Update the entity over the web API.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json');
     $this->assertResponse(204);
 
     // Re-load updated entity from the database.
@@ -77,20 +75,20 @@ public function testPatchUpdate() {
     $this->assertNull($entity->field_test_text->value, 'Test field has been cleared.');
 
     // Try to update a non-existing entity with ID 9999.
-    $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/hal+json');
     $this->assertResponse(404);
     $loaded_entity = entity_load($entity_type, 9999, TRUE);
     $this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
 
     // Try to update an entity without proper permissions.
     $this->drupalLogout();
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json');
     $this->assertResponse(403);
 
     // Try to update a resource which is not web API enabled.
     $this->enableService(FALSE);
     $this->drupalLogin($account);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json');
     $this->assertResponse(404);
   }
 
@@ -120,9 +118,9 @@ public function testPutUpdate() {
     $update_entity->uuid->value = $entity->uuid();
     $update_entity->id->value = $entity->id();
 
-    $serialized = $serializer->serialize($update_entity, 'drupal_jsonld');
+    $serialized = $serializer->serialize($update_entity, 'hal_json');
     // Update the entity over the web API.
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json');
     $this->assertResponse(204);
 
     // Re-load the updated entity from the database.
@@ -138,8 +136,8 @@ public function testPutUpdate() {
 
     // Try to delete a property.
     unset($update_entity->field_test_text);
-    $serialized = $serializer->serialize($update_entity, 'drupal_jsonld');
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json');
+    $serialized = $serializer->serialize($update_entity, 'hal_json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json');
     $this->assertResponse(204);
 
     // Re-load the updated entity from the database.
@@ -147,20 +145,20 @@ public function testPutUpdate() {
     $this->assertTrue($entity->field_test_text->isEmpty(), 'Property has been deleted.');
 
     // Try to create an entity with ID 9999.
-    $this->httpRequest('entity/' . $entity_type . '/9999', 'PUT', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/9999', 'PUT', $serialized, 'application/hal+json');
     $this->assertResponse(404);
     $loaded_entity = entity_load($entity_type, 9999, TRUE);
     $this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
 
     // Try to update an entity without proper permissions.
     $this->drupalLogout();
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json');
     $this->assertResponse(403);
 
     // Try to update a resource which is not web API enabled.
     $this->enableService(FALSE);
     $this->drupalLogin($account);
-    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json');
+    $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json');
     $this->assertResponse(404);
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php b/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php
index 4aeaaa7..c38db82 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php
@@ -25,7 +25,7 @@ class StyleSerializerTest extends PluginTestBase {
    *
    * @var array
    */
-  public static $modules = array('views_ui', 'entity_test', 'jsonld', 'rest_test_views');
+  public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views');
 
   /**
    * Views used by this test.
@@ -127,13 +127,9 @@ public function testSerializerResponses() {
 
     $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
 
-    $expected = $serializer->serialize($entities, 'jsonld');
-    $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/ld+json'));
-    $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.');
-
-    $expected = $serializer->serialize($entities, 'drupal_jsonld');
-    $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/vnd.drupal.ld+json'));
-    $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.');
+    $expected = $serializer->serialize($entities, 'hal_json');
+    $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/hal+json'));
+    $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
   }
 
   /**
diff --git a/core/modules/rest/rest.info b/core/modules/rest/rest.info
index c450567..7b0b88d 100644
--- a/core/modules/rest/rest.info
+++ b/core/modules/rest/rest.info
@@ -3,7 +3,5 @@ description = Exposes entities and other resources as RESTful web API
 package = Core
 version = VERSION
 core = 8.x
-; @todo Remove this dependency once hard coding to JSON-LD is gone.
-dependencies[] = jsonld
 dependencies[] = serialization
 configure = admin/config/services/rest
diff --git a/core/modules/rest/rest.module b/core/modules/rest/rest.module
index 233db32..43b32f4 100644
--- a/core/modules/rest/rest.module
+++ b/core/modules/rest/rest.module
@@ -69,7 +69,7 @@ function rest_help($path, $arg) {
       $output .= '<h3>' . t('Example uses') . '</h3>';
       $output .= '<dl>';
       $output .= '<dt>' . t('An HTTP GET request can be used to get a node') . '</dt>';
-      $output .= '<dd><code>curl -H "Accept: application/vnd.drupal.ld+json" --include --request GET --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '</code></dd>';
+      $output .= '<dd><code>curl -H "Accept: application/hal+json" --include --request GET --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '</code></dd>';
       $output .= '<dt>' . t('An HTTP DELETE request can be used to delete a node') . '</dt>';
       $output .= '<dd><code>curl --include --request DELETE --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '</code></dd>';
       $output .= '</dl>';
