diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
index e15fe84..87a787c 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\TypedData\MapDataDefinition;
 
 /**
  * Defines the 'map' entity field type.
@@ -22,8 +23,12 @@ class MapItem extends FieldItemBase {
    * {@inheritdoc}
    */
   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
-    // The properties are dynamic and can not be defined statically.
-    return [];
+    return [
+      // All map fields store their data in a single 'value' property, but this
+      // itself expands to an arbitrary map.
+      // @see \Drupal\Core\TypedData\Plugin\DataType\Map
+      'value' => MapDataDefinition::create()->setLabel(t('Serialized array of values')),
+    ];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index dbf0d5f..228722f 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -168,9 +168,8 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name,
       // a shorter string than the serialized form, so array access is faster.
       $parts[] = json_encode($settings);
     }
-    // Property path for the requested data object. When creating a list item,
-    // use 0 in the key as all items look the same.
-    $parts[] = $object->getPropertyPath() . '.' . (is_numeric($property_name) ? 0 : $property_name);
+    // Property path for the requested data object.
+    $parts[] = $object->getPropertyPath() . '.' . $property_name;
     $key = implode(':', $parts);
 
     // Create the prototype if needed.
diff --git a/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php b/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php
new file mode 100644
index 0000000..0804607
--- /dev/null
+++ b/core/modules/link/tests/src/Kernel/LinkItemSerializationTest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\Tests\link\Kernel;
+
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\link\LinkItemInterface;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Tests\field\Kernel\FieldKernelTestBase;
+
+/**
+ * Tests link field serialization.
+ *
+ * @group link
+ */
+class LinkItemSerializationTest extends FieldKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['link', 'serialization'];
+
+  /**
+   * The serializer type.
+   */
+  protected $serializer;
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->serializer = \Drupal::service('serializer');
+
+    // Create a generic, external, and internal link fields for validation.
+    FieldStorageConfig::create([
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test',
+      'type' => 'link',
+    ])->save();
+
+    FieldConfig::create([
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_test',
+      'bundle' => 'entity_test',
+      'settings' => ['link_type' => LinkItemInterface::LINK_GENERIC],
+    ])->save();
+  }
+
+  /**
+   * Tests the serialization.
+   */
+  public function testLinkSerialization() {
+    // Create entity.
+    $entity = EntityTest::create();
+    $url = 'https://www.drupal.org?test_param=test_value';
+    $parsed_url = UrlHelper::parse($url);
+    $title = $this->randomMachineName();
+    $class = $this->randomMachineName();
+    $entity->field_test->uri = $parsed_url['path'];
+    $entity->field_test->title = $title;
+    $entity->field_test->first()
+      ->get('options')
+      ->set('query', $parsed_url['query']);
+    $entity->field_test->first()
+      ->get('options')
+      ->set('attributes', ['class' => $class]);
+    $entity->save();
+    $serialized = $this->serializer->serialize($entity, 'json');
+    $deserialized = $this->serializer->deserialize($serialized, EntityTest::class, 'json');
+    $options_expected = [
+      'query' => $parsed_url['query'],
+      'attributes' => ['class' => $class],
+    ];
+    $this->assertSame($options_expected, $deserialized->field_test->options);
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 669382d..a3436fa 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -914,8 +914,7 @@ public function testPost() {
         if ($created_entity->hasField($field_name)) {
           // Subset, not same, because we can e.g. send just the target_id for the
           // bundle in a POST request; the response will include more properties.
-          $this->assertArraySubset(static::castToString($field_normalization), $created_entity->get($field_name)
-            ->getValue(), TRUE);
+          $this->assertArraySubset(static::castToString($field_normalization), $created_entity_normalization[$field_name], TRUE);
         }
       }
     }
@@ -1175,7 +1174,7 @@ public function testPatch() {
       if ($updated_entity->hasField($field_name)) {
         // Subset, not same, because we can e.g. send just the target_id for the
         // bundle in a PATCH request; the response will include more properties.
-        $this->assertArraySubset(static::castToString($field_normalization), $updated_entity->get($field_name)->getValue(), TRUE);
+        $this->assertArraySubset(static::castToString($field_normalization), $updated_entity_normalization[$field_name], TRUE);
       }
     }
     // Ensure that fields do not get deleted if they're not present in the PATCH
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestMapItemNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestMapItemNormalizerTest.php
new file mode 100644
index 0000000..194bce3
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestMapItemNormalizerTest.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+
+/**
+ * Test that MapItem are correctly exposed in REST.
+ *
+ * @group rest
+ */
+class EntityTestMapItemNormalizerTest extends EntityTestResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  protected static $mapValue = [
+    'key1' => 'value',
+    'key2' => 'no, val you',
+    'nested' => [
+      'bird' => 'robin',
+      'doll' => 'Russian',
+    ],
+  ];
+
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $expected = parent::getExpectedNormalizedEntity();
+    // The 'non_exposed_value' property in test field type will not return in
+    // normalization because setExposed(TRUE) was not called for this property.
+    // @see \Drupal\entity_test\Plugin\Field\FieldType\ExposedPropertyTestFieldItem::propertyDefinitions
+    $expected['field_map'] = [
+      static::$mapValue
+    ];
+    return $expected;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    if (!FieldStorageConfig::loadByName('entity_test', 'field_map')) {
+      FieldStorageConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_map',
+        'type' => 'map',
+        'cardinality' => 1,
+        'translatable' => FALSE,
+      ])->save();
+      FieldConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_map',
+        'bundle' => 'entity_test',
+        'label' => 'Test field with map property',
+      ])->save();
+    }
+
+    $entity = parent::createEntity();
+    $entity->field_map = [
+      'value' => static::$mapValue,
+    ];
+    $entity->save();
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    return parent::getNormalizedPostEntity() + [
+      'field_map' => [
+        static::$mapValue,
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
index 0b1f967..fa50726 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
@@ -59,7 +59,15 @@ protected function createEntity() {
       'id' => 'llama',
       'title' => 'Llama Gabilondo',
       'description' => 'Llama Gabilondo',
-      'link' => 'https://nl.wikipedia.org/wiki/Llama',
+      'link' => [
+        'uri' => 'https://nl.wikipedia.org/wiki/Llama',
+        'options' => [
+          'fragment' => 'a-fragment',
+          'attributes' => [
+            'class' => ['example-class'],
+          ],
+        ],
+      ],
       'weight' => 0,
       'menu_name' => 'main',
     ]);
@@ -81,6 +89,12 @@ protected function getNormalizedPostEntity() {
       'link' => [
         [
           'uri' => 'http://www.urbandictionary.com/define.php?term=drama%20llama',
+          'options' => [
+            'fragment' => 'a-fragment',
+            'attributes' => [
+              'class' => ['example-class'],
+            ],
+          ],
         ],
       ],
       'bundle' => [
@@ -115,7 +129,12 @@ protected function getExpectedNormalizedEntity() {
         [
           'uri' => 'https://nl.wikipedia.org/wiki/Llama',
           'title' => NULL,
-          'options' => [],
+          'options' => [
+            'fragment' => 'a-fragment',
+            'attributes' => [
+              'class' => ['example-class'],
+            ],
+          ],
         ],
       ],
       'weight' => [
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Shortcut/ShortcutResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Shortcut/ShortcutResourceTestBase.php
index 36be5b5..de307e6 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Shortcut/ShortcutResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Shortcut/ShortcutResourceTestBase.php
@@ -58,6 +58,9 @@ protected function createEntity() {
       'weight' => -20,
       'link' => [
         'uri' => 'internal:/admin/content/comment',
+        'options' => [
+          'fragment' => 'new',
+        ],
       ],
     ]);
     $shortcut->save();
@@ -96,7 +99,9 @@ protected function getExpectedNormalizedEntity() {
         [
           'uri' => 'internal:/admin/content/comment',
           'title' => NULL,
-          'options' => [],
+          'options' => [
+            'fragment' => 'new',
+          ],
         ],
       ],
       'weight' => [
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index dca6094..c374043 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -52,6 +52,12 @@ services:
       # Priority must be higher than serialization.normalizer.field but less
       # than hal field normalizer.
       - { name: normalizer, priority: 9 }
+  serializer.normalizer.map_item:
+    class: Drupal\serialization\Normalizer\MapItemNormalizer
+    tags:
+      # Priority must be higher than serializer.normalizer.field_item and lower
+      # than hal normalizers.
+      - { name: normalizer, priority: 8 }
   serializer.normalizer.timestamp_item:
     class: Drupal\serialization\Normalizer\TimestampItemNormalizer
     tags:
@@ -71,6 +77,24 @@ services:
     class: Drupal\serialization\Normalizer\TypedDataNormalizer
     tags:
       - { name: normalizer }
+  serializer.normalizer.map.propertyless:
+    class: Drupal\serialization\Normalizer\PropertylessMapNormalizer
+    tags:
+    # This normalizer must be higher than serializer.normalizer.complex_data so
+    # that serializer.normalizer.complex_data is not used for Map objects that
+    # do not provide property definitions. While giving this normalizer a
+    # priority of 1 would work when considering only core's normalizers it would
+    # not allow for any other custom normalizers to be given priority between
+    # serializer.normalizer.complex_data and this normalizer. Giving this
+    # normalizer an especially high priority allows normalizers with a range of
+    # priorities for other classes that do not provide property definitions.
+    # Giving this normalizer an especially high priority will NOT cause it to
+    # be used for most classes that extend Map, such any class that extends
+    # \Drupal\Core\Field\FieldItemBase, because those objects will have
+    # property definitions.
+    #
+    # @see \Drupal\serialization\Normalizer\MapNormalizer::supportsNormalization
+      - { name: normalizer, priority: 20}
   serializer.encoder.json:
     class: Drupal\serialization\Encoder\JsonEncoder
     tags:
diff --git a/core/modules/serialization/src/Normalizer/MapItemNormalizer.php b/core/modules/serialization/src/Normalizer/MapItemNormalizer.php
new file mode 100644
index 0000000..0c501d6
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/MapItemNormalizer.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Field\Plugin\Field\FieldType\MapItem;
+
+/**
+ * @internal
+ */
+class MapItemNormalizer extends FieldItemNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = MapItem::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field_item, $format = NULL, array $context = []) {
+    // Remove the level of indirection: pretend the arbitrary keys stored in the
+    // map are the properties on this field.
+    return parent::normalize($field_item, $format, $context)['value'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    // Prepare for storage: all received data must be assigned to this field
+    // type's sole property: 'value'.
+    return ['value' => $data];
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/PropertylessMapNormalizer.php b/core/modules/serialization/src/Normalizer/PropertylessMapNormalizer.php
new file mode 100644
index 0000000..7309794
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/PropertylessMapNormalizer.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Converts propertyless Map objects into arrays.
+ *
+ * This normalizer only supports Map objects that do not have have property
+ * definitions.
+ */
+class PropertylessMapNormalizer extends TypedDataNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = Map::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsNormalization($data, $format = NULL) {
+    /* @var \Drupal\Core\TypedData\Plugin\DataType\Map $data  */
+    if (parent::supportsNormalization($data, $format)) {
+      $definition = $data->getDataDefinition();
+      if ($definition instanceof ComplexDataDefinitionInterface && empty($definition->getPropertyDefinitions())) {
+        // Map objects without properties defined must be treated specially: the
+        // top-level keys stored must be considered the properties during
+        // normalization. The parent ::normalize() method does this.
+        return TRUE;
+      }
+      else {
+        // Map objects with properties defined can be handled by
+        // \Drupal\serialization\Normalizer\ComplexDataNormalizer::normalize().
+        return FALSE;
+      }
+    }
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/MapDataNormalizerTest.php b/core/modules/serialization/tests/src/Kernel/MapDataNormalizerTest.php
new file mode 100644
index 0000000..11e3353
--- /dev/null
+++ b/core/modules/serialization/tests/src/Kernel/MapDataNormalizerTest.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\Tests\serialization\Kernel;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\MapDataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @group typedData
+ */
+class MapDataNormalizerTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+
+  public static $modules = ['system', 'serialization'];
+
+  /**
+   * The serializer service.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The typed data manager.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataManagerInterface
+   */
+  protected $typedDataManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->serializer = \Drupal::service('serializer');
+    $this->typedDataManager = \Drupal::typedDataManager();
+  }
+
+  /**
+   * Tests whether map data can be normalized.
+   */
+  public function testMapNormalize() {
+    $typed_data = $this->buildExampleTypedData();
+    $data = $this->serializer->normalize($typed_data, 'json');
+    $expect_value = [
+      'key1' => 'value1',
+      'key2' => 'value2',
+      'key3' => 3,
+      'key4' => [
+        0 => TRUE,
+        1 => 'value6',
+        'key7' => 'value7',
+      ],
+    ];
+    $this->assertSame($expect_value, $data);
+  }
+
+  /**
+   * Test whether map data with properties can be normalized.
+   */
+  public function testMapWithPropertiesNormalize() {
+    $typed_data = $this->buildExampleTypedDataWithProperties();
+    $data = $this->serializer->normalize($typed_data, 'json');
+    $expect_value = [
+      'key1' => 'value1',
+      'key2' => 'value2',
+      'key3' => 3,
+      'key4' => [
+        0 => TRUE,
+        1 => 'value6',
+        'key7' => 'value7',
+      ],
+    ];
+    $this->assertSame($expect_value, $data);
+  }
+
+  /**
+   * Builds some example typed data object with no properties.
+   */
+  protected function buildExampleTypedData() {
+    $tree = [
+      'key1' => 'value1',
+      'key2' => 'value2',
+      'key3' => 3,
+      'key4' => [
+        0 => TRUE,
+        1 => 'value6',
+        'key7' => 'value7',
+      ],
+    ];
+    $map_data_definition = MapDataDefinition::create();
+    $typed_data = $this->typedDataManager->create(
+      $map_data_definition,
+      $tree,
+      'test name'
+    );
+    return $typed_data;
+  }
+
+  /**
+   * Builds some example typed data object with properties.
+   */
+  protected function buildExampleTypedDataWithProperties() {
+    $tree = [
+      'key1' => 'value1',
+      'key2' => 'value2',
+      'key3' => 3,
+      'key4' => [
+        0 => TRUE,
+        1 => 'value6',
+        'key7' => 'value7',
+      ],
+    ];
+    $map_data_definition = MapDataDefinition::create()
+      ->setPropertyDefinition('key1', DataDefinition::create('string'))
+      ->setPropertyDefinition('key2', DataDefinition::create('string'))
+      ->setPropertyDefinition('key3', DataDefinition::create('integer'))
+      ->setPropertyDefinition('key4', MapDataDefinition::create()
+      ->setPropertyDefinition(0, DataDefinition::create('boolean'))
+      ->setPropertyDefinition(1, DataDefinition::create('string'))
+      ->setPropertyDefinition('key7', DataDefinition::create('string'))
+    );
+
+    $typed_data = $this->typedDataManager->create(
+      $map_data_definition,
+      $tree,
+      'test name'
+    );
+
+    return $typed_data;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/MapTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/MapTestItem.php
new file mode 100644
index 0000000..dfb1295
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/MapTestItem.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\entity_test\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\MapItem;
+use Drupal\Core\TypedData\MapDataDefinition;
+
+/**
+ * Defines the 'map_test' field type.
+ *
+ * @FieldType(
+ *   id = "map_test",
+ *   label = @Translation("Map Test"),
+ *   description = @Translation("Another dummy field type."),
+ * )
+ */
+class MapTestItem extends MapItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'value' => MapDataDefinition::create()->setLabel(t('Freeform')),
+    ];
+  }
+
+}
