diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index dbf0d5fec5..228722fdf0 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 0000000000..0804607295
--- /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/EntityTest/EntityTestMapItemNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestMapItemNormalizerTest.php
new file mode 100644
index 0000000000..a8ed8597de
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestMapItemNormalizerTest.php
@@ -0,0 +1,95 @@
+<?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'] = [
+      [
+        'value' => 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_test',
+        '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' => [
+        [
+          'value' => static::$mapValue,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index dca6094787..4da7dabb47 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -71,6 +71,24 @@ services:
     class: Drupal\serialization\Normalizer\TypedDataNormalizer
     tags:
       - { name: normalizer }
+  serializer.normalizer.map:
+    class: Drupal\serialization\Normalizer\MapNormalizer
+    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/MapNormalizer.php b/core/modules/serialization/src/Normalizer/MapNormalizer.php
new file mode 100644
index 0000000000..24c1a76bd1
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/MapNormalizer.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
+use Drupal\Core\TypedData\Plugin\DataType\Map;
+
+/**
+ * Converts Map objects into arrays.
+ *
+ * This normalizer only supports Map objects that do not have have property
+ * definitions.
+ */
+class MapNormalizer 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())) {
+        return TRUE;
+      }
+    }
+    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 0000000000..20fa9e5adb
--- /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/MapItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/MapItem.php
new file mode 100644
index 0000000000..e09fdf70b1
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/MapItem.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\entity_test\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+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 MapItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'value' => MapDataDefinition::create()->setLabel(t('Freeform')),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'columns' => [
+        'value' => [
+          'description' => 'Serialized array of stuff.',
+          'type' => 'blob',
+          'size' => 'big',
+          'serialize' => TRUE,
+        ],
+      ],
+    ];
+  }
+
+}
