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 afc8d50a27498e7364d9ce6a6b0f1e1d07f6184b..5a1fe319e1c5b286e2b590f0832a96feefc93570 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
@@ -7,17 +7,19 @@
 
 namespace Drupal\rest\Plugin\rest\resource;
 
-use Drupal\rest\Plugin\ResourceBase;
 use Drupal\Core\Annotation\Plugin;
-use Symfony\Component\HttpFoundation\Response;
+use Drupal\Core\Annotation\Translation;
+use Drupal\rest\Plugin\ResourceBase;
+use Drupal\rest\ResourceResponse;
+
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Provides a resource for database watchdog log entries.
  *
  * @Plugin(
- *  id = "dblog",
- *  label = "Watchdog database log"
+ *   id = "dblog",
+ *   label = @Translation("Watchdog database log")
  * )
  */
 class DBLogResource extends ResourceBase {
@@ -38,23 +40,24 @@ public function routes() {
    *
    * Returns a watchdog log entry for the specified ID.
    *
-   * @return \Symfony\Component\HttpFoundation\Response
-   *   The response object.
+   * @return \Drupal\rest\ResourceResponse
+   *   The response containing the log entry.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
   public function get($id = NULL) {
     if ($id) {
-      $result = db_select('watchdog', 'w')
-        ->condition('wid', $id)
-        ->fields('w')
-        ->execute()
-        ->fetchAll();
-      if (empty($result)) {
-        throw new NotFoundHttpException('Not Found');
+      $result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
+        ->fetchObject();
+      if (!empty($result)) {
+        // 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));
+        return $response;
       }
-      // @todo remove hard coded format here.
-      return new Response(drupal_json_encode($result[0]), 200, array('Content-Type' => 'application/json'));
     }
+    throw new NotFoundHttpException('Not Found');
   }
 }
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 352438908c1065df8205cf1e0951f2a51bcb807a..689fd84ab9b902517c48d0276a88c9bca35405c9 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
@@ -8,9 +8,10 @@
 namespace Drupal\rest\Plugin\rest\resource;
 
 use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\rest\Plugin\ResourceBase;
-use Symfony\Component\HttpFoundation\Response;
+use Drupal\rest\ResourceResponse;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
@@ -18,21 +19,41 @@
  * Represents entities as resources.
  *
  * @Plugin(
- *  id = "entity",
- *  label = "Entity",
- *  derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
+ *   id = "entity",
+ *   label = @Translation("Entity"),
+ *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
  * )
  */
 class EntityResource extends ResourceBase {
 
+  /**
+   * Responds to entity GET requests.
+   *
+   * @param mixed $id
+   *   The entity ID.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The response containing the loaded entity.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function get($id) {
+    $definition = $this->getDefinition();
+    $entity = entity_load($definition['entity_type'], $id);
+    if ($entity) {
+      return new ResourceResponse($entity);
+    }
+    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
+  }
+
   /**
    * Responds to entity DELETE requests.
    *
    * @param mixed $id
    *   The entity ID.
    *
-   * @return \Symfony\Component\HttpFoundation\Response
-   *   The response object.
+   * @return \Drupal\rest\ResourceResponse
+   *   The HTTP response object.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
@@ -43,7 +64,7 @@ public function delete($id) {
       try {
         $entity->delete();
         // Delete responses have an empty body.
-        return new Response('', 204);
+        return new ResourceResponse(NULL, 204);
       }
       catch (EntityStorageException $e) {
         throw new HttpException(500, 'Internal Server Error', $e);
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index 6bea36a92764affa9a0747b825ab68b5112054f2..a374d14c61459c3a1339c04ed1ffaa31b343ca5a 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -38,12 +38,26 @@ public function handle($plugin, Request $request, $id = NULL) {
       $resource = $this->container
         ->get('plugin.manager.rest')
         ->getInstance(array('id' => $plugin));
+      $received = $request->getContent();
+      // @todo De-serialization should happen here if the request is supposed
+      // to carry incoming data.
       try {
-        return $resource->{$method}($id);
+        $response = $resource->{$method}($id, $received);
       }
       catch (HttpException $e) {
         return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
       }
+      $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');
+        $response->setContent($output);
+        $response->headers->set('Content-Type', 'application/vnd.drupal.ld+json');
+      }
+      return $response;
     }
     return new Response('Access Denied', 403);
   }
diff --git a/core/modules/rest/lib/Drupal/rest/ResourceResponse.php b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..a93f25e84b9ffcf4ec0595afb7a331e5d26b6a85
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\rest\ResourceResponse.
+ */
+
+namespace Drupal\rest;
+
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Contains data for serialization before sending the response.
+ */
+class ResourceResponse extends Response {
+
+  /**
+   * Response data that should be serialized.
+   *
+   * @var mixed
+   */
+  protected $responseData;
+
+  /**
+   * Constructor for ResourceResponse objects.
+   *
+   * @param mixed $data
+   *   Response data that should be serialized.
+   * @param int $status
+   *   The response status code.
+   * @param array $headers
+   *   An array of response headers.
+   */
+  public function __construct($data = NULL, $status = 200, $headers = array()) {
+    $this->responseData = $data;
+    parent::__construct('', $status, $headers);
+  }
+
+  /**
+   * Returns response data that should be serialized.
+   *
+   * @return mixed
+   *   Response data that should be serialized.
+   */
+  public function getResponseData() {
+    return $this->responseData;
+  }
+}
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
index a18d064a6aa6fa2596ea954b5f578b55288b13b4..500239743d9cf8a15725f7732f1da1a87578b822 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('rest', 'dblog');
+  public static $modules = array('jsonld', 'rest', 'dblog');
 
   public static function getInfo() {
     return array(
@@ -32,17 +32,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
     // Enable web API for the watchdog resource.
-    $config = config('rest');
-    $config->set('resources', array(
-      'dblog' => 'dblog',
-    ));
-    $config->save();
-
-    // Rebuild routing cache, so that the web API paths are available.
-    drupal_container()->get('router.builder')->rebuild();
-    // Reset the Simpletest permission cache, so that the new resource
-    // permissions get picked up.
-    drupal_static_reset('checkPermissions');
+    $this->enableService('dblog');
   }
 
   /**
@@ -60,15 +50,16 @@ public function testWatchdog() {
     $account = $this->drupalCreateUser(array('restful get dblog'));
     $this->drupalLogin($account);
 
-    $response = $this->httpRequest("dblog/$id", 'GET');
+    $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/json');
     $this->assertResponse(200);
+    $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.');
     $this->assertEqual($log['message'], 'Test message', 'Log message text is correct.');
 
     // Request an unknown log entry.
-    $response = $this->httpRequest("dblog/9999", 'GET');
+    $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/json');
     $this->assertResponse(404);
     $this->assertEqual($response, '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 187c38667aa827223ad75eb2b00e4dffb5cdac87..4c039c296be55e5783cc7771b199f6b5cb5f9ee2 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -34,18 +34,7 @@ public static function getInfo() {
    */
   public function testDelete() {
     foreach (entity_get_info() as $entity_type => $info) {
-      // Enable web API for this entity type.
-      $config = config('rest');
-      $config->set('resources', array(
-        'entity:' . $entity_type => 'entity:' . $entity_type,
-      ));
-      $config->save();
-
-      // Rebuild routing cache, so that the web API paths are available.
-      drupal_container()->get('router.builder')->rebuild();
-      // Reset the Simpletest permission cache, so that the new resource
-      // permissions get picked up.
-      drupal_static_reset('checkPermissions');
+      $this->enableService('entity:' . $entity_type);
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
@@ -81,6 +70,7 @@ public function testDelete() {
       $this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
     }
     // Try to delete a resource which is not web API enabled.
+    $this->enableService(FALSE);
     $account = $this->drupalCreateUser();
     // Reset cURL here because it is confused from our previously used cURL
     // options.
@@ -88,32 +78,7 @@ public function testDelete() {
     $this->drupalLogin($account);
     $this->httpRequest('entity/user/' . $account->id(), 'DELETE');
     $user = entity_load('user', $account->id(), TRUE);
-    $this->assertEqual($account->id(), $user->id());
+    $this->assertEqual($account->id(), $user->id(), 'User still exists in the database.');
     $this->assertResponse(404);
   }
-
-  /**
-   * 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..
-   *
-   * @return \Drupal\Core\Entity\EntityInterface
-   *   The new entity object.
-   */
-  protected function entityCreate($entity_type) {
-    switch ($entity_type) {
-      case 'entity_test':
-        return entity_create('entity_test', array('name' => 'test', 'user_id' => 1));
-      case 'node':
-        return entity_create('node', array('title' => $this->randomString()));
-      case 'user':
-        return entity_create('user', array('name' => $this->randomName()));
-      default:
-        return entity_create($entity_type, array());
-    }
-  }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index 3bc875646563228470fc8ef098ae7465b7b04c33..acf48022303370bee4237c30e0508dcd923375fe 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -14,6 +14,13 @@
  */
 abstract class RESTTestBase extends WebTestBase {
 
+  /**
+   * Stores HTTP response headers from the last HTTP request.
+   *
+   * @var array
+   */
+  protected $responseHeaders;
+
   /**
    * Helper function to issue a HTTP request with simpletest's cURL.
    *
@@ -31,36 +38,136 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
       case 'GET':
         // Set query if there are additional GET parameters.
         $options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE);
-        return $this->curlExec(array(
+        $curl_options = array(
           CURLOPT_HTTPGET => TRUE,
           CURLOPT_URL => url($url, $options),
-          CURLOPT_NOBODY => FALSE)
+          CURLOPT_NOBODY => FALSE
         );
+        break;
+
       case 'POST':
-        return $this->curlExec(array(
+        $curl_options = array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_POST => TRUE,
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
           CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
-        ));
+        );
+        break;
+
       case 'PUT':
-        return $this->curlExec(array(
+        $curl_options = array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_CUSTOMREQUEST => 'PUT',
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
           CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
-        ));
+        );
+        break;
+
       case 'DELETE':
-        return $this->curlExec(array(
+        $curl_options = array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_CUSTOMREQUEST => 'DELETE',
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
-        ));
+        );
+        break;
     }
+    // Include all HTTP headers in the response.
+    $curl_options[CURLOPT_HEADER] = TRUE;
+
+    $response = $this->curlExec($curl_options);
+
+    list($header, $body) = explode("\r\n\r\n", $response, 2);
+    $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]) : '';
+    }
+
+    $this->verbose($method . ' request to: ' . $url .
+      '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
+      '<hr />Response headers: ' . $header .
+      '<hr />Response body: ' . $body);
+
+    return $body;
+  }
+
+  /**
+   * 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..
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The new entity object.
+   */
+  protected function entityCreate($entity_type) {
+    switch ($entity_type) {
+      case 'entity_test':
+        return entity_create('entity_test', array('name' => $this->randomName(), 'user_id' => 1));
+      case 'node':
+        return entity_create('node', array('title' => $this->randomString()));
+      case 'user':
+        return entity_create('user', array('name' => $this->randomName()));
+      default:
+        return entity_create($entity_type, array());
+    }
+  }
+
+  /**
+   * Enables the web service interface for a specific entity type.
+   *
+   * @param string|FALSE $resource_type
+   *   The resource type that should get web API enabled or FALSE to disable all
+   *   resource types.
+   */
+  protected function enableService($resource_type) {
+    // Enable web API for this entity type.
+    $config = config('rest');
+    if ($resource_type) {
+      $config->set('resources', array(
+        $resource_type => $resource_type,
+      ));
+    }
+    else {
+      $config->set('resources', array());
+    }
+    $config->save();
+
+    // Rebuild routing cache, so that the web API paths are available.
+    drupal_container()->get('router.builder')->rebuild();
+    // Reset the Simpletest permission cache, so that the new resource
+    // permissions get picked up.
+    drupal_static_reset('checkPermissions');
+  }
+
+  /**
+   * Check if a HTTP response header exists and has the expected value.
+   *
+   * @param string $header
+   *   The header key, example: Content-Type
+   * @param string $value
+   *   The header value.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   * @param string $group
+   *   (optional) The group this message is in, which is displayed in a column
+   *   in test output. Use 'Debug' to indicate this is debugging output. Do not
+   *   translate this string. Defaults to 'Other'; most tests do not override
+   *   this default.
+   *
+   * @return bool
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   */
+  protected function assertHeader($header, $value, $message = '', $group = 'Browser') {
+    $match = isset($this->responseHeaders[$header]) && $this->responseHeaders[$header] == $value;
+    return $this->assertTrue($match, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found.', $group);
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..28ffcea36c4a8efedc89bf890d199a492d36cf6d
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\rest\test\ReadTest.
+ */
+
+namespace Drupal\rest\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+
+/**
+ * Tests resource read operations on test entities, nodes and users.
+ */
+class ReadTest extends RESTTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('jsonld', 'rest', 'entity_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Read resource',
+      'description' => 'Tests the retrieval of resources.',
+      'group' => 'REST',
+    );
+  }
+
+  /**
+   * Tests several valid and invalid read requests on all entity types.
+   */
+  public function testRead() {
+    // @todo once EntityNG is implemented for other entity types use the full
+    // entity_get_info() for all entity types here.
+    $entity_test_info = entity_get_info('entity_test');
+    $entity_info = array('entity_test' => $entity_test_info);
+    foreach ($entity_info as $entity_type => $info) {
+      $this->enableService('entity:' . $entity_type);
+      // Create a user account that has the required permissions to delete
+      // resources via the web API.
+      $account = $this->drupalCreateUser(array('restful get entity:' . $entity_type));
+      // Reset cURL here because it is confused from our previously used cURL
+      // options.
+      unset($this->curlHandle);
+      $this->drupalLogin($account);
+
+      // Create an entity programmatically.
+      $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');
+      $this->assertResponse('200', 'HTTP response code is correct.');
+      $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.
+      $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
+
+      // Try to read an entity that does not exist.
+      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/ld+json');
+      $this->assertResponse(404);
+      $this->assertEqual($response, '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');
+      $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();
+    // Reset cURL here because it is confused from our previously used cURL
+    // options.
+    unset($this->curlHandle);
+    $this->drupalLogin($account);
+    $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/vnd.drupal.ld+json');
+    $this->assertResponse(404);
+    $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
+  }
+}
diff --git a/core/modules/rest/rest.info b/core/modules/rest/rest.info
index 56c650c316fb180404ad820b859727b67ac9d9bb..c0bf4bdf7fd627cbf73e6d2cb28c1bf135d3f08d 100644
--- a/core/modules/rest/rest.info
+++ b/core/modules/rest/rest.info
@@ -3,4 +3,6 @@ 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
 configure = admin/config/services/rest