diff --git a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
index c94492e..d701206 100644
--- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
+++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -4,6 +4,7 @@ namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Uuid\Uuid;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
 use Drupal\jsonapi\Context\CurrentContext;
@@ -14,6 +15,7 @@ use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -106,6 +108,12 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
       $normalized = $data['data']['attributes'];
     }
 
+    if (!empty($data['data']['id'])) {
+      $resource_type = $this->resourceTypeRepository->getByTypeName($data['data']['type']);
+      $uuid_key = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId())->getKey('uuid');
+      $normalized[$uuid_key] = $data['data']['id'];
+    }
+
     if (!empty($data['data']['relationships'])) {
       // Turn all single object relationship data fields into an array of
       // objects.
@@ -307,6 +315,12 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
     if (!isset($document['data']['type'])) {
       throw new BadRequestHttpException("Resource object must include a \"type\".");
     }
+    // Ensure that the client provided ID is a valid UUID.
+    if (isset($document['data']['id']) && !Uuid::isValid($document['data']['id'])) {
+      // This should be a 422 response, but the JSON API specification dictates
+      // a 403 Forbidden response. We follow the specification.
+      throw new AccessDeniedHttpException('IDs should be properly generated and formatted UUIDs as described in RFC 4122.');
+    }
   }
 
 }
diff --git a/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
index 4f49fcf..f007c98 100644
--- a/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
+++ b/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\jsonapi\Unit\Normalizer;
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\jsonapi\Context\FieldResolver;
@@ -13,6 +14,7 @@ use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\Context\CurrentContext;
 use Drupal\Tests\UnitTestCase;
 use Prophecy\Argument;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Serializer\SerializerInterface;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -64,8 +66,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
         return $result;
       });
     $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
-    $entity_type_manager->getStorage('node')
-      ->willReturn($entity_storage->reveal());
+    $entity_type_manager->getStorage('node')->willReturn($entity_storage->reveal());
+    $entity_type = $this->prophesize(EntityTypeInterface::class);
+    $entity_type->getKey('uuid')->willReturn('uuid');
+    $entity_type_manager->getDefinition('node')->willReturn($entity_type->reveal());
 
     $current_route = $this->prophesize(Route::class);
     $current_route->getDefault('_on_relationship')->willReturn(FALSE);
@@ -120,7 +124,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
             'attributes' => ['title' => 'dummy_title'],
           ],
         ],
-        ['title' => 'dummy_title'],
+        [
+          'title' => 'dummy_title',
+          'uuid' => 'e1a613f6-f2b9-4e17-9d33-727eb6509d8b',
+        ],
       ],
       [
         [
@@ -131,10 +138,11 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
           ],
         ],
         [
+          'uuid' => '0676d1bf-55b3-4bbc-9fbc-3df10f4599d5',
           'field_dummy' => [
-          [
-            'target_id' => 1,
-          ],
+            [
+              'target_id' => 1,
+            ],
           ],
         ],
       ],
@@ -160,13 +168,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
           ],
         ],
         [
+          'uuid' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
           'field_dummy' => [
-          [
-            'target_id' => 1,
-          ],
-          [
-            'target_id' => 2,
-          ],
+            ['target_id' => 1],
+            ['target_id' => 2],
           ],
         ],
       ],
@@ -193,18 +198,70 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
           ],
         ],
         [
+          'uuid' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
           'field_dummy' => [
-          [
-            'target_id' => 1,
-            'foo' => 'bar',
-          ],
-          [
-            'target_id' => 2,
-          ],
+            [
+              'target_id' => 1,
+              'foo' => 'bar',
+            ],
+            ['target_id' => 2],
           ],
         ],
       ],
     ];
   }
 
+  /**
+   * Ensures only valid UUIDs can be specified.
+   *
+   * @param string $id
+   *   The input UUID. May be invalid.
+   * @param bool $expect_exception
+   *   Whether to expect an exception.
+   *
+   * @covers ::denormalize
+   * @dataProvider denormalizeUuidProvider
+   */
+  public function testDenormalizeUuid($id, $expect_exception) {
+    $data['data'] = (isset($id)) ?
+      ['type' => 'node--article', 'id' => $id] :
+      ['type' => 'node--article'];
+
+    if ($expect_exception) {
+      $this->setExpectedException(
+        AccessDeniedHttpException::class,
+        'IDs should be properly generated and formatted UUIDs as described in RFC 4122.'
+      );
+    }
+
+    $denormalized = $this->normalizer->denormalize($data, NULL, 'api_json', [
+      'resource_type' => new ResourceType(
+        $this->randomMachineName(),
+        $this->randomMachineName(),
+        FieldableEntityInterface::class
+      ),
+    ]);
+
+    if (isset($id)) {
+      $this->assertSame($id, $denormalized['uuid']);
+    }
+    else {
+      $this->assertArrayNotHasKey('uuid', $denormalized);
+    }
+  }
+
+  /**
+   * Provides test cases for testDenormalizeUuid.
+   */
+  public function denormalizeUuidProvider() {
+    return [
+      'valid' => ['76dd5c18-ea1b-4150-9e75-b21958a2b836', FALSE],
+      'missing' => [NULL, FALSE],
+      'invalid_empty' => ['', TRUE],
+      'invalid_alpha' => ['invalid', TRUE],
+      'invalid_numeric' => [1234, TRUE],
+      'invalid_alphanumeric' => ['abc123', TRUE],
+    ];
+  }
+
 }
