diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEncoder.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEncoder.php
index 2b9ed2a..8aa289e 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEncoder.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEncoder.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\jsonld;
 
-use Symfony\Component\Serializer\Encoder\EncoderInterface;
 use Symfony\Component\Serializer\Encoder\JsonEncoder;
 
 /**
@@ -15,7 +14,7 @@
  *
  * Simply respond to JSON-LD requests using the JSON encoder.
  */
-class JsonldEncoder extends JsonEncoder implements EncoderInterface {
+class JsonldEncoder extends JsonEncoder {
 
   /**
    * The formats that this Encoder supports.
@@ -25,15 +24,17 @@ class JsonldEncoder extends JsonEncoder implements EncoderInterface {
   static protected $format = array('jsonld', 'drupal_jsonld');
 
   /**
-   * Check whether the request is for JSON-LD.
-   *
-   * @param string $format
-   *   The short name of the format returned by ContentNegotiation.
-   *
-   * @return bool
-   *   Returns TRUE if the encoder can handle the request.
+   * Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsEncoding()
    */
   public function supportsEncoding($format) {
     return in_array($format, static::$format);
   }
+
+  /**
+   * Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsDecoding()
+   */
+  public function supportsDecoding($format) {
+    return in_array($format, static::$format);
+  }
+
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php
index b85941b..2ab909e 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php
@@ -7,14 +7,20 @@
 
 namespace Drupal\jsonld;
 
-use Drupal\Core\Entity\EntityNG;
 use Drupal\jsonld\JsonldNormalizerBase;
-use Symfony\Component\Serializer\Exception\RuntimeException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Converts the Drupal entity object structure to JSON-LD array structure.
  */
-class JsonldEntityNormalizer extends JsonldNormalizerBase {
+class JsonldEntityNormalizer extends JsonldNormalizerBase implements DenormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected static $supportedInterfaceOrClass = 'Drupal\Core\Entity\EntityInterface';
 
   /**
    * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
@@ -23,17 +29,103 @@ public function normalize($entity, $format = NULL) {
     $entityWrapper = new JsonldEntityWrapper($entity, $format, $this->serializer);
 
     $attributes = $entityWrapper->getProperties();
-    $attributes = array('@id' => $entityWrapper->getId()) + $attributes;
+    $attributes = array(
+      '@id' => $entityWrapper->getId(),
+      '@type' => $entityWrapper->getTypeUri(),
+    ) + $attributes;
     return $attributes;
   }
 
   /**
-   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization()
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
    */
-  public function supportsNormalization($data, $format = NULL) {
-    // @todo Switch to EntityInterface once all entity types are converted to
-    // EntityNG.
-    return parent::supportsNormalization($data, $format) && ($data instanceof EntityNG);
+  public function denormalize($data, $class, $format = null) {
+    if (!isset($data['@type'])) {
+      // @todo Throw an exception?
+    }
+
+    // Every bundle has a type, identified by URI. A schema which provides more
+    // information about the type can be requested from that URI. From that
+    // schema, the entity type and bundle name are extracted.
+    $typeUri = is_array($data['@type']) ? $data['@type'][0] : $data['@type'];
+    // @todo Instead of manually parsing the URI use an approach as discussed in
+    // http://drupal.org/node/1852812
+    $parts = explode('/', $typeUri);
+    $bundle = array_pop($parts);
+    $entity_type = array_pop($parts);
+
+    $values = array(
+      'type' => $bundle,
+    );
+    // If the data specifies a default language, use it to create the entity.
+    if (isset($data['langcode'])) {
+      $values['langcode'] = $data['langcode'][LANGUAGE_NOT_SPECIFIED][0]['value'];
+    }
+    // Otherwise, if the default language is not specified but there are
+    // translations of field values, explicitly set the entity's default
+    // language to the site's default language. This is required to enable
+    // field translation on this entity.
+    else if ($this->containsTranslation($data)) {
+      $values['langcode'] = language(LANGUAGE_TYPE_CONTENT)->langcode;
+    }
+    $entity = entity_create($entity_type, $values);
+
+    // For each attribute in the JSON-LD, add the values as fields to the newly
+    // created entity. It is assumed that the JSON attribute names are the same
+    // as the site's field names.
+    // @todo Possibly switch to URI expansion of attribute names.
+    foreach ($data as $fieldName => $incomingFieldValues) {
+      // Skip the JSON-LD specific terms, which start with '@'.
+      if ($fieldName[0] === '@') {
+        continue;
+      }
+
+      // Figure out the designated class for this field type, which is used by
+      // the Serializer to determine which Denormalizer to use.
+      // @todo Is there a better way to get the field type's associated class?
+      $fieldItemClass = get_class($entity->get($fieldName)->offsetGet(0));
+      // Iterate through the language keyed values and add them to the entity.
+      // The vnd.drupal.ld+json mime type will always use language keys, per
+      // http://drupal.org/node/1838700.
+      foreach ($incomingFieldValues as $langcode => $incomingFieldItems) {
+        $fieldValue = $this->serializer->denormalize($incomingFieldItems, $fieldItemClass, $format);
+        $entity->getTranslation($langcode)
+          ->set($fieldName, $fieldValue);
+      }
+    }
+    return $entity;
   }
 
+  /**
+   * Determine whether incoming data contains translated content.
+   *
+   * @param array $data
+   *   The incoming data.
+   *
+   * @return bool
+   *   Whether or not this data contains translated content.
+   */
+  protected function containsTranslation($data) {
+    // Langcodes which do not represent a translation of the entity.
+    $defaultLangcodes = array(
+      LANGUAGE_DEFAULT,
+      LANGUAGE_NOT_SPECIFIED,
+      LANGUAGE_NOT_APPLICABLE,
+      language(LANGUAGE_TYPE_CONTENT)->langcode,
+    );
+
+    // Combine the langcodes from the field value keys in a single array.
+    $fieldLangcodes = array();
+    foreach ($data as $propertyName => $property) {
+      //@todo Once @context has been added, check whether this property
+      // corresponds to an annotation instead. This will allow us to support
+      // incoming data that doesn't use language annotations.
+      if ('@' !== $propertyName[0]) {
+        $fieldLangcodes += array_keys($property);
+      }
+    }
+
+    $translationLangcodes = array_diff($fieldLangcodes, $defaultLangcodes);
+    return !empty($translationLangcodes);
+  }
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php
index 1e6d27c..e28ce95 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php
@@ -7,14 +7,23 @@
 
 namespace Drupal\jsonld;
 
-use Drupal\Core\Entity\Field\Type\EntityReferenceItem;
+use Drupal\Core\Entity\EntityNG;
 use Drupal\jsonld\JsonldNormalizerBase;
+use ReflectionClass;
 use Symfony\Component\Serializer\Exception\RuntimeException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Converts an EntityReferenceItem to a JSON-LD array structure.
  */
-class JsonldEntityReferenceNormalizer extends JsonldNormalizerBase {
+class JsonldEntityReferenceNormalizer extends JsonldNormalizerBase implements DenormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected static $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\Type\EntityReferenceItem';
 
   /**
    * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
@@ -32,10 +41,19 @@ public function normalize($object, $format = NULL) {
   }
 
   /**
-   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization()
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
+   */
+  public function denormalize($data, $class, $format = null) {
+    // @todo Support denormalization for Entity Reference.
+    return array();
+  }
+
+  /**
+   * Overrides \Drupal\jsonld\JsonldNormalizerBase::supportsDenormalization()
    */
-  public function supportsNormalization($data, $format = NULL) {
-    return parent::supportsNormalization($data, $format) && ($data instanceof EntityReferenceItem);
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    $reflection = new ReflectionClass($type);
+    return in_array($format, static::$format) && ($reflection->getName() == static::$supportedInterfaceOrClass || $reflection->isSubclassOf(static::$supportedInterfaceOrClass));
   }
 
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php
index 65bf6e0..cfffa8c 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php
@@ -11,6 +11,13 @@
 
 /**
  * Provide an interface for JsonldNormalizer to get required properties.
+ *
+ * @todo Eventually, this class should be removed. It allows both the
+ * EntityNormalizer and the EntityReferenceNormalizer to have access to the
+ * same functions. If an $options parameter is added to the serialize
+ * signature, as requested in https://github.com/symfony/symfony/pull/4938,
+ * then the EntityReferenceNormalizer could simply call
+ * EntityNormalizer::normalize(), passing in the referenced entity.
  */
 class JsonldEntityWrapper {
 
@@ -63,6 +70,18 @@ public function getId() {
   }
 
   /**
+   * Get the type URI.
+   *
+   * @todo update or remove this method once the schema dependency to RDF module
+   * is sorted out.
+   */
+  public function getTypeUri() {
+    $entity_type = $this->entity->entityType();
+    $bundle = $this->entity->bundle();
+    return url('site-schema/content-staging/' . $entity_type . '/' . $bundle, array('absolute' => TRUE));
+  }
+
+  /**
    * Get properties, excluding JSON-LD specific properties.
    *
    * @return array
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldFieldItemNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldFieldItemNormalizer.php
index 3fac801..bf421a1 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldFieldItemNormalizer.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldFieldItemNormalizer.php
@@ -10,11 +10,19 @@
 use Drupal\Core\Entity\Field\FieldItemInterface;
 use Drupal\jsonld\JsonldNormalizerBase;
 use Symfony\Component\Serializer\Exception\RuntimeException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Converts the Drupal entity object structure to JSON-LD array structure.
  */
-class JsonldFieldItemNormalizer extends JsonldNormalizerBase {
+class JsonldFieldItemNormalizer extends JsonldNormalizerBase implements DenormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected static $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\FieldItemInterface';
 
   /**
    * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
@@ -24,10 +32,11 @@ public function normalize($object, $format = NULL) {
   }
 
   /**
-   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization()
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
    */
-  public function supportsNormalization($data, $format = NULL) {
-    return parent::supportsNormalization($data, $format) && ($data instanceof FieldItemInterface);
+  public function denormalize($data, $class, $format = null) {
+    // For most fields, the field items array should simply be returned as is.
+    return $data;
   }
 
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php
index 2508459..bb9ae5a 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php
@@ -7,8 +7,7 @@
 
 namespace Drupal\jsonld;
 
-use Drupal\Core\Entity\EntityNG;
-use Symfony\Component\Serializer\Exception\RuntimeException;
+use ReflectionClass;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
 
@@ -18,6 +17,13 @@
 abstract class JsonldNormalizerBase extends SerializerAwareNormalizer implements NormalizerInterface {
 
   /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected static $supportedInterfaceOrClass;
+
+  /**
    * The formats that this Normalizer supports.
    *
    * @var array
@@ -28,7 +34,18 @@
    * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
    */
   public function supportsNormalization($data, $format = NULL) {
-    return is_object($data) && in_array($format, static::$format);
+    return is_object($data) && in_array($format, static::$format) && ($data instanceof static::$supportedInterfaceOrClass);
   }
 
+  /**
+   * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization()
+   *
+   * This class doesn't implement DenormalizerInterface, but most of its child
+   * classes do, so this method is implemented at this level to reduce code
+   * duplication.
+   */
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    $reflection = new ReflectionClass($type);
+    return in_array($format, static::$format) && $reflection->implementsInterface(static::$supportedInterfaceOrClass);
+  }
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldNormalizerTestBase.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldNormalizerTestBase.php
deleted file mode 100644
index 229f77b..0000000
--- a/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldNormalizerTestBase.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\jsonld\Tests\JsonldTestBase.
- */
-
-namespace Drupal\jsonld\Tests;
-
-use Drupal\simpletest\WebTestBase;
-use Drupal\config\Tests\ConfigEntityTest;
-
-/**
- * Parent class for JSON-LD tests.
- */
-abstract class JsonldNormalizerTestBase extends WebTestBase {
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('language', 'entity_test');
-
-  /**
-   * Get the Entity ID.
-   *
-   * @param Drupal\Core\Entity\EntityNG $entity
-   *   Entity to get URI for.
-   *
-   * @return string
-   *   Return the entity URI.
-   */
-  protected function getEntityId($entity) {
-    global $base_url;
-    $uriInfo = $entity->uri();
-    return $base_url . '/' . $uriInfo['path'];
-  }
-
-}
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/DrupalJsonldNormalizerTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php
similarity index 64%
rename from core/modules/jsonld/lib/Drupal/jsonld/Tests/DrupalJsonldNormalizerTest.php
rename to core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php
index 8845342..9ecfda5 100644
--- a/core/modules/jsonld/lib/Drupal/jsonld/Tests/DrupalJsonldNormalizerTest.php
+++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php
@@ -2,23 +2,30 @@
 
 /**
  * @file
- * Definition of Drupal\jsonld\Tests\DrupalJsonldNormalizerTest.
+ * Contains Drupal\jsonld\Tests\NormalizeDenormalizeTest.
  */
 
 namespace Drupal\jsonld\Tests;
 
-use Drupal\config\Tests\ConfigEntityTest;
 use Drupal\Core\Language\Language;
+use Drupal\jsonld\JsonldEncoder;
 use Drupal\jsonld\JsonldEntityNormalizer;
 use Drupal\jsonld\JsonldEntityReferenceNormalizer;
 use Drupal\jsonld\JsonldFieldItemNormalizer;
-use Drupal\jsonld\Tests\JsonldNormalizerTestBase;
+use Drupal\simpletest\WebTestBase;
 use Symfony\Component\Serializer\Serializer;
 
 /**
  * Test the vendor specific JSON-LD normalizer.
  */
-class DrupalJsonldNormalizerTest extends JsonldNormalizerTestBase {
+class NormalizeDenormalizeTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('language', 'entity_test');
 
   /**
    * The format being tested.
@@ -32,8 +39,8 @@ class DrupalJsonldNormalizerTest extends JsonldNormalizerTestBase {
 
   public static function getInfo() {
     return array(
-      'name' => 'vnd.drupal.ld+json Normalization',
-      'description' => "Test Drupal's vendor specific JSON-LD normalizer.",
+      'name' => 'Normalize/Denormalize Test',
+      'description' => "Test that entities can be normalized/denormalized in JSON-LD.",
       'group' => 'JSON-LD',
     );
   }
@@ -49,42 +56,21 @@ function setUp() {
       'field_item' => new JsonldFieldItemNormalizer(),
       'entity' => new JsonldEntityNormalizer(),
     );
-    $serializer = new Serializer($this->normalizers);
+    $serializer = new Serializer($this->normalizers, array(new JsonldEncoder()));
     $this->normalizers['entity']->setSerializer($serializer);
-  }
 
-  /**
-   * Tests the supportsNormalization function.
-   */
-  public function testSupportsNormalization() {
-    $format = static::$format;
-    $supportedEntity = entity_create('entity_test', array());
-    $unsupportedEntity = new ConfigEntityTest();
-    $field = $supportedEntity->get('uuid');
-    $entityreferenceField = $supportedEntity->get('user_id');
-
-    // Supported entity.
-    $this->assertTrue($this->normalizers['entity']->supportsNormalization($supportedEntity, static::$format), "Entity normalization is supported for $format on content entities.");
-    // Unsupported entity.
-    $this->assertFalse($this->normalizers['entity']->supportsNormalization($unsupportedEntity, static::$format), "Normalization is not supported for other entity types.");
-
-    // Field item.
-    $this->assertTrue($this->normalizers['field_item']->supportsNormalization($field->offsetGet(0), static::$format), "Field item normalization is supported for $format.");
-    // Entity reference field item.
-    $this->assertTrue($this->normalizers['entityreference']->supportsNormalization($entityreferenceField->offsetGet(0), static::$format), "Entity reference field item normalization is supported for $format.");
-  }
-
-  /**
-   * Tests the normalize function.
-   */
-  public function testNormalize() {
     // Add German as a language.
     $language = new Language(array(
       'langcode' => 'de',
       'name' => 'Deutsch',
     ));
     language_save($language);
+  }
 
+  /**
+   * Tests the normalize function.
+   */
+  public function testNormalize() {
     // Create a German entity.
     $values = array(
       'langcode' => 'de',
@@ -159,4 +145,51 @@ public function testNormalize() {
     $this->assertEqual($normalized['field_test_text'], $expectedArray['field_test_text'], 'Field with properties is nested correctly.');
   }
 
+  function testDenormalize() {
+    $incomingData = array(
+      '@type' => url('jsonld-test/content-staging/entity_test/entity_test', array('absolute' => TRUE)),
+      'name' => array(
+        'en' => array(
+          array(
+            'value' => $this->randomName(),
+          ),
+        ),
+        'de' => array(
+          array(
+            'value' => $this->randomName(),
+          ),
+        ),
+      ),
+      'field_test_text' => array(
+        'und' => array(
+          array(
+            'value' => $this->randomName(),
+            'format' => 'full_html',
+          ),
+        ),
+      ),
+    );
+
+    $entity = $this->normalizers['entity']->denormalize($incomingData, 'Drupal\Core\Entity\EntityNG', static::$format);
+    $this->assertEqual('entity_test', $entity->bundle(), "Denormalize creates entity with correct bundle.");
+    $this->assertEqual($incomingData['name']['en'], $entity->get('name')->getValue(), "Translatable field denormalized correctly in default language.");
+    $this->assertEqual($incomingData['name']['de'], $entity->getTranslation('de')->get('name')->getValue(), "Translatable field denormalized correctly in translation language.");
+    $this->assertEqual($incomingData['field_test_text']['und'], $entity->get('field_test_text')->getValue(), "Untranslatable field denormalized correctly.");
+  }
+
+  /**
+   * Get the Entity ID.
+   *
+   * @param Drupal\Core\Entity\EntityNG $entity
+   *   Entity to get URI for.
+   *
+   * @return string
+   *   Return the entity URI.
+   */
+  protected function getEntityId($entity) {
+    global $base_url;
+    $uriInfo = $entity->uri();
+    return $base_url . '/' . $uriInfo['path'];
+  }
+
 }
diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php
new file mode 100644
index 0000000..b0d4474
--- /dev/null
+++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\jsonld\Tests\SupportsSerializationTest.
+ */
+
+namespace Drupal\jsonld\Tests;
+
+use Drupal\config\Tests\ConfigEntityTest;
+use Drupal\jsonld\JsonldEntityNormalizer;
+use Drupal\jsonld\JsonldEntityReferenceNormalizer;
+use Drupal\jsonld\JsonldFieldItemNormalizer;
+use Drupal\simpletest\WebTestBase;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Test the vendor specific JSON-LD normalizer.
+ */
+class SupportsSerializationTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_test');
+
+  /**
+   * The format being tested.
+   */
+  protected static $format = 'drupal_jsonld';
+
+  /**
+   * The Normalizers to be tested.
+   */
+  protected $normalizers;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Supports class/format serialization test',
+      'description' => "Test that normalizers and encoders support expected classes and formats.",
+      'group' => 'JSON-LD',
+    );
+  }
+
+  /**
+   * Add the normalizer to be tested.
+   */
+  function setUp() {
+    parent::setUp();
+
+    $this->normalizers = array(
+      'entityreference' => new JsonldEntityReferenceNormalizer(),
+      'field_item' => new JsonldFieldItemNormalizer(),
+      'entity' => new JsonldEntityNormalizer(),
+    );
+    $serializer = new Serializer($this->normalizers);
+    $this->normalizers['entity']->setSerializer($serializer);
+  }
+
+  /**
+   * Tests the supportsNormalization function.
+   */
+  public function testSupportsNormalization() {
+    $format = static::$format;
+    $supportedEntity = entity_create('entity_test', array());
+    $unsupportedEntity = new ConfigEntityTest();
+    $field = $supportedEntity->get('uuid');
+    $entityreferenceField = $supportedEntity->get('user_id');
+
+    // Supported entity.
+    $this->assertTrue($this->normalizers['entity']->supportsNormalization($supportedEntity, static::$format), "Entity normalization is supported for $format on content entities.");
+    // Unsupported entity.
+    $this->assertFalse($this->normalizers['entity']->supportsNormalization($unsupportedEntity, static::$format), "Normalization is not supported for other entity types.");
+
+    // Field item.
+    $this->assertTrue($this->normalizers['field_item']->supportsNormalization($field->offsetGet(0), static::$format), "Field item normalization is supported for $format.");
+    // Entity reference field item.
+    $this->assertTrue($this->normalizers['entityreference']->supportsNormalization($entityreferenceField->offsetGet(0), static::$format), "Entity reference field item normalization is supported for $format.");
+  }
+
+  /**
+   * Tests the supportsDenormalization function.
+   */
+  public function testSupportsDenormalization() {
+    $format = static::$format;
+    $data = array();
+    $supportedEntityClass = 'Drupal\Core\Entity\EntityNG';
+    $unsupportedEntityClass = 'Drupal\config\Tests\ConfigEntityTest';
+    $fieldClass = 'Drupal\Core\Entity\Field\Type\StringItem';
+    $entityreferenceFieldClass = 'Drupal\Core\Entity\Field\Type\EntityReferenceItem';
+
+    // Supported entity.
+    $this->assertTrue($this->normalizers['entity']->supportsDenormalization($data, $supportedEntityClass, static::$format), "Entity denormalization is supported for $format on content entities.");
+    // Unsupported entity.
+    $this->assertFalse($this->normalizers['entity']->supportsDenormalization($data, $unsupportedEntityClass, static::$format), "Denormalization is not supported for other entity types.");
+
+    // Field item.
+    $this->assertTrue($this->normalizers['field_item']->supportsDenormalization($data, $fieldClass, static::$format), "Field item denormalization is supported for $format.");
+    // Entity reference field item.
+    $this->assertTrue($this->normalizers['entityreference']->supportsDenormalization($data, $entityreferenceFieldClass, static::$format), "Entity reference field item denormalization is supported for $format.");
+  }
+
+}
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
index 2b81b14..310d843 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
@@ -43,6 +43,7 @@ public function getDerivativeDefinitions(array $base_plugin_definition) {
         $this->derivatives[$entity_type] = array(
           'id' => 'entity:' . $entity_type,
           'entity_type' => $entity_type,
+          'serialization_class' => $entity_info['class'],
           'label' => $entity_info['label'],
         );
         $this->derivatives[$entity_type] += $base_plugin_definition;
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
index 35be5df..1509af8 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -54,26 +54,45 @@ public function permissions() {
   public function routes() {
     $collection = new RouteCollection();
 
+    $name = strtr($this->plugin_id, ':', '.');
+    $prefix = strtr($this->plugin_id, ':', '/');
     $methods = $this->requestMethods();
     foreach ($methods as $method) {
       $lower_method = strtolower($method);
       // Only expose routes where the HTTP request method exists on the plugin.
       if (method_exists($this, $lower_method)) {
-        $prefix = strtr($this->plugin_id, ':', '/');
-        $route = new Route("/$prefix/{id}", array(
-          '_controller' => 'Drupal\rest\RequestHandler::handle',
-          // @todo Once http://drupal.org/node/1793520 is committed we will have
-          // route object avaialble in the controller so 'plugin' property
-          // should be changed to '_plugin'.
-          // @see RequestHandler::handle().
-          'plugin' => $this->plugin_id,
-        ), array(
-          // The HTTP method is a requirement for this route.
-          '_method' => $method,
-          '_permission' => "restful $lower_method $this->plugin_id",
-        ));
+        // Special case for resource creation via POST: Add a route that does
+        // not require an ID.
+        if ($method == 'POST') {
+          $route = new Route("/$prefix", array(
+            '_controller' => 'Drupal\rest\RequestHandler::handle',
+            // @todo Once http://drupal.org/node/1793520 is committed we will have
+            // route object available in the controller so 'plugin' property
+            // should be changed to '_plugin'.
+            // @see RequestHandler::handle().
+            'plugin' => $this->plugin_id,
+            'id' => NULL,
+          ), array(
+            // The HTTP method is a requirement for this route.
+            '_method' => $method,
+            '_permission' => "restful $lower_method $this->plugin_id",
+          ));
+        }
+        else {
+          $route = new Route("/$prefix/{id}", array(
+            '_controller' => 'Drupal\rest\RequestHandler::handle',
+            // @todo Once http://drupal.org/node/1793520 is committed we will have
+            // route object available in the controller so 'plugin' property
+            // should be changed to '_plugin'.
+            // @see RequestHandler::handle().
+            'plugin' => $this->plugin_id,
+          ), array(
+            // The HTTP method is a requirement for this route.
+            '_method' => $method,
+            '_permission' => "restful $lower_method $this->plugin_id",
+          ));
+        }
 
-        $name = strtr($this->plugin_id, ':', '.');
         $collection->add("$name.$method", $route);
       }
     }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
index 5a1fe31..13af57b 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -47,14 +47,14 @@ public function routes() {
    */
   public function get($id = NULL) {
     if ($id) {
-      $result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
+      $record = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
         ->fetchObject();
-      if (!empty($result)) {
+      if (!empty($record)) {
         // Serialization is done here, so we indicate with NULL that there is no
         // subsequent serialization necessary.
         $response = new ResourceResponse(NULL, 200, array('Content-Type' => 'application/json'));
         // @todo remove hard coded format here.
-        $response->setContent(drupal_json_encode($result));
+        $response->setContent(drupal_json_encode($record));
         return $response;
       }
     }
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 689fd84..df8cc5a 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
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Annotation\Plugin;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\rest\ResourceResponse;
@@ -21,6 +22,7 @@
  * @Plugin(
  *   id = "entity",
  *   label = @Translation("Entity"),
+ *   serialization_class = "Drupal\Core\Entity\Entity",
  *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
  * )
  */
@@ -47,6 +49,37 @@ public function get($id) {
   }
 
   /**
+   * Responds to entity POST requests.
+   *
+   * @param mixed $id
+   *   Ignored.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The HTTP response object.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function post($id, EntityInterface $entity) {
+    // Verify that the deserialized entity is of the type that we expect to
+    // prevent security issues.
+    $definition = $this->getDefinition();
+    if ($entity->entityType() != $definition['entity_type']) {
+      throw new HttpException(400, 'Bad Request');
+    }
+    try {
+      $entity->save();
+      $url = url(strtr($this->plugin_id, ':', '/') . '/' . $entity->id(), array('absolute' => TRUE));
+      // 201 Created responses have an empty body.
+      return new ResourceResponse(NULL, 201, array('Location' => $url));
+    }
+    catch (EntityStorageException $e) {
+      throw new HttpException(500, 'Internal Server Error', $e);
+    }
+  }
+
+  /**
    * Responds to entity DELETE requests.
    *
    * @param mixed $id
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index f30684d..8a33361 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -37,19 +37,28 @@ public function handle($plugin, Request $request, $id = NULL) {
     $resource = $this->container
       ->get('plugin.manager.rest')
       ->getInstance(array('id' => $plugin));
+
+    // Deserialze incoming data if available.
+    $serializer = $this->container->get('serializer');
     $received = $request->getContent();
-    // @todo De-serialization should happen here if the request is supposed
-    // to carry incoming data.
+    $unserialized = NULL;
+    if (!empty($received)) {
+      $definition = $resource->getDefinition();
+      $class = $definition['serialization_class'];
+      $unserialized = $serializer->deserialize($received, $class, 'drupal_jsonld');
+    }
+
+    // Invoke the operation on the resource plugin.
     try {
-      $response = $resource->{$method}($id, $received);
+      $response = $resource->{$method}($id, $unserialized, $request);
     }
     catch (HttpException $e) {
       return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
     }
+
+    // Serialize the outgoing data for the response, if available.
     $data = $response->getResponseData();
     if ($data != NULL) {
-      // Serialize the response data.
-      $serializer = $this->container->get('serializer');
       // @todo Replace the format here with something we get from the HTTP
       //   Accept headers. See http://drupal.org/node/1833440
       $output = $serializer->serialize($data, 'drupal_jsonld');
diff --git a/core/modules/rest/lib/Drupal/rest/ResourceResponse.php b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
index a93f25e..d79653a 100644
--- a/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
+++ b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
@@ -11,6 +11,11 @@
 
 /**
  * Contains data for serialization before sending the response.
+ *
+ * We do not want to abuse the $content property on the Response class to store
+ * our response data. $content implies that the provided data must either be a
+ * string or an object with a __toString() method, which is not a requirement
+ * for data used here.
  */
 class ResourceResponse extends Response {
 
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
new file mode 100644
index 0000000..2d59244
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\rest\test\CreateTest.
+ */
+
+namespace Drupal\rest\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+
+/**
+ * Tests resource creation on user, node and test entities.
+ */
+class CreateTest extends RESTTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('rest', 'entity_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Create resource',
+      'description' => 'Tests the creation of resources.',
+      'group' => 'REST',
+    );
+  }
+
+  /**
+   * Tests several valid and invalid create requests on all entity types.
+   */
+  public function testCreate() {
+    $serializer = drupal_container()->get('serializer');
+    // @todo once EntityNG is implemented for other entity types test all other
+    // entity types here as well.
+    $entity_type = 'entity_test';
+
+    $this->enableService('entity:' . $entity_type);
+    // Create a user account that has the required permissions to create
+    // resources via the web API.
+    $account = $this->drupalCreateUser(array('restful post entity:' . $entity_type));
+    $this->drupalLogin($account);
+
+    $entity_values = $this->entityValues($entity_type);
+    $entity = entity_create($entity_type, $entity_values);
+    $serialized = $serializer->serialize($entity, 'drupal_jsonld');
+    // @todo HACK: remove once the site schema routes from RDF module work.
+    $serialized = str_replace('site-schema\\/content-staging\\/entity_test\\/entity_test', 'jsonld-test\\/content-staging\\/entity_test\\/entity_test', $serialized);
+    // Create the entity over the web API.
+    $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json');
+    $this->assertResponse('201', 'HTTP response code is correct.');
+
+    // Get the new entity ID from the location header and try to read it from
+    // the database.
+    $location_url = $this->responseHeaders['location'];
+    $url_parts = explode('/', $location_url);
+    $id = end($url_parts);
+    $loaded_entity = entity_load($entity_type, $id);
+    $this->assertNotIdentical(FALSE, $loaded_entity, 'The new ' . $entity_type . ' was found in the database.');
+    $this->assertEqual($entity->uuid(), $loaded_entity->uuid(), 'UUID of created entity is correct.');
+    // @todo Remove the user reference field for now until deserialization for
+    // entity references is implemented.
+    unset($entity_values['user_id']);
+    foreach ($entity_values as $property => $value) {
+      $actual_value = $loaded_entity->get($property);
+      $this->assertEqual($value, $actual_value->value, 'Created property ' . $property . ' expected: ' . $value . ', actual: ' . $actual_value->value);
+    }
+
+    // Try to create an entity without proper permissions.
+    $this->drupalLogout();
+    $response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json');
+    $this->assertResponse(403);
+
+    // 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->assertResponse(404);
+
+    // @todo Once EntityNG is implemented for other entity types add a security
+    // test. It should not be possible for example to create a test entity on a
+    // node resource route.
+  }
+}
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
index 5002397..9ea37c1 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
@@ -52,7 +52,7 @@ public function testWatchdog() {
 
     $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/json');
     $this->assertResponse(200);
-    $this->assertHeader('Content-Type', 'application/json');
+    $this->assertHeader('content-type', 'application/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.');
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index acf4802..3640e48 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -85,7 +85,9 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
     $header_lines = explode("\r\n", $header);
     foreach ($header_lines as $line) {
       $parts = explode(':', $line, 2);
-      $this->responseHeaders[$parts[0]] = isset($parts[1]) ? trim($parts[1]) : '';
+      // Store the header keys lower cased to be more robust. Headers are case
+      // insensitive according to RFC 2616.
+      $this->responseHeaders[strtolower($parts[0])] = isset($parts[1]) ? trim($parts[1]) : '';
     }
 
     $this->verbose($method . ' request to: ' . $url .
@@ -99,25 +101,38 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
   /**
    * Creates entity objects based on their types.
    *
-   * Required properties differ from entity type to entity type, so we keep a
-   * minimum mapping here.
-   *
    * @param string $entity_type
-   *   The type of the entity that should be created..
+   *   The type of the entity that should be created.
    *
    * @return \Drupal\Core\Entity\EntityInterface
    *   The new entity object.
    */
   protected function entityCreate($entity_type) {
+    return entity_create($entity_type, $this->entityValues($entity_type));
+  }
+
+  /**
+   * Provides an array of suitable property values for an entity type.
+   *
+   * Required properties differ from entity type to entity type, so we keep a
+   * minimum mapping here.
+   *
+   * @param string $entity_type
+   *   The type of the entity that should be created.
+   *
+   * @return array
+   *   An array of values keyed by property name.
+   */
+  protected function entityValues($entity_type) {
     switch ($entity_type) {
       case 'entity_test':
-        return entity_create('entity_test', array('name' => $this->randomName(), 'user_id' => 1));
+        return array('name' => $this->randomName(), 'user_id' => 1);
       case 'node':
-        return entity_create('node', array('title' => $this->randomString()));
+        return array('title' => $this->randomString());
       case 'user':
-        return entity_create('user', array('name' => $this->randomName()));
+        return array('name' => $this->randomName());
       default:
-        return entity_create($entity_type, array());
+        return array();
     }
   }
 
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
index 0c710e5..7ebe5f2 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -53,7 +53,7 @@ public function testRead() {
       // Read it over the web API.
       $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
       $this->assertResponse('200', 'HTTP response code is correct.');
-      $this->assertHeader('Content-Type', 'application/vnd.drupal.ld+json');
+      $this->assertHeader('content-type', 'application/vnd.drupal.ld+json');
       $data = drupal_json_decode($response);
       // Only assert one example property here, other properties should be
       // checked in serialization tests.
