diff --git a/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php b/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php
index 6b47af0171..cc2cc7fee6 100644
--- a/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php
+++ b/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php
@@ -3,6 +3,7 @@
 namespace Drupal\Component\Plugin\Discovery;
 
 use Drupal\Component\Plugin\Definition\DerivablePluginDefinitionInterface;
+use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
 use Drupal\Component\Plugin\Exception\InvalidDeriverException;
 
 /**
@@ -55,7 +56,7 @@ public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
     // $base_plugin_id.
     $plugin_definition = $this->decorated->getDefinition($plugin_id, FALSE);
 
-    list($base_plugin_id, $derivative_id) = $this->decodePluginId($plugin_id);
+    [$base_plugin_id, $derivative_id] = $this->decodePluginId($plugin_id);
     $base_plugin_definition = $this->decorated->getDefinition($base_plugin_id, $exception_on_invalid);
     if ($base_plugin_definition) {
       $deriver = $this->getDeriver($base_plugin_id, $base_plugin_definition);
@@ -69,6 +70,9 @@ public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
         else {
           $plugin_definition = $derivative_plugin_definition;
         }
+        // It is vital that derivative plugin definitions contain derivative
+        // plugin IDs. Enforce it here, because not all derivers do it.
+        $this->setPluginIdOnDefinition($plugin_definition, $plugin_id);
       }
     }
 
@@ -99,14 +103,17 @@ protected function getDerivatives(array $base_plugin_definitions) {
       $deriver = $this->getDeriver($base_plugin_id, $plugin_definition);
       if ($deriver) {
         $derivative_definitions = $deriver->getDerivativeDefinitions($plugin_definition);
-        foreach ($derivative_definitions as $derivative_id => $derivative_definition) {
+        foreach ($derivative_definitions as $derivative_id => $derivative_plugin_definition) {
           $plugin_id = $this->encodePluginId($base_plugin_id, $derivative_id);
           // Use this definition as defaults if a plugin already defined
           // itself as this derivative.
           if ($derivative_id && isset($base_plugin_definitions[$plugin_id])) {
-            $derivative_definition = $this->mergeDerivativeDefinition($base_plugin_definitions[$plugin_id], $derivative_definition);
+            $derivative_plugin_definition = $this->mergeDerivativeDefinition($base_plugin_definitions[$plugin_id], $derivative_plugin_definition);
           }
-          $plugin_definitions[$plugin_id] = $derivative_definition;
+          // It is vital that derivative plugin definitions contain derivative
+          // plugin IDs. Enforce it here, because not all derivers do it.
+          $this->setPluginIdOnDefinition($derivative_plugin_definition, $plugin_id);
+          $plugin_definitions[$plugin_id] = $derivative_plugin_definition;
         }
       }
       // If a plugin already defined itself as a derivative it might already
@@ -227,21 +234,48 @@ protected function getDeriverClass($base_definition) {
   /**
    * Merges a base and derivative definition, taking into account empty values.
    *
-   * @param array $base_plugin_definition
+   * @param mixed[]|\Drupal\Component\Plugin\Definition\PluginDefinitionInterface $base_plugin_definition
    *   The base plugin definition.
-   * @param array $derivative_definition
+   * @param mixed[]|\Drupal\Component\Plugin\Definition\MergeablePluginDefinitionInterface $derivative_definition
    *   The derivative plugin definition.
    *
-   * @return array
+   * @return mixed[]|\Drupal\Component\Plugin\Definition\PluginDefinitionInterface
    *   The merged definition.
    */
   protected function mergeDerivativeDefinition($base_plugin_definition, $derivative_definition) {
-    // Use this definition as defaults if a plugin already defined itself as
-    // this derivative, but filter out empty values first.
-    $filtered_base = array_filter($base_plugin_definition);
-    $derivative_definition = $filtered_base + ($derivative_definition ?: []);
-    // Add back any empty keys that the derivative didn't have.
-    return $derivative_definition + $base_plugin_definition;
+    if (is_array($base_plugin_definition) && is_array($derivative_definition)) {
+      // Use this definition as defaults if a plugin already defined itself as
+      // this derivative, but filter out empty values first.
+      $filtered_base = array_filter($base_plugin_definition);
+      $derivative_definition = $filtered_base + ($derivative_definition ?: []);
+      // Add back any empty keys that the derivative didn't have.
+      return $derivative_definition + $base_plugin_definition;
+    }
+    elseif ($derivative_definition instanceof MergeablePluginDefinitionInterface && $base_plugin_definition instanceof PluginDefinitionInterface) {
+      return $derivative_definition->mergeDefaultDefinition($base_plugin_definition);
+    }
+    else {
+      $base_plugin_definition_type = is_object($base_plugin_definition) ? get_class($base_plugin_definition) : gettype($base_plugin_definition);
+      $derivative_plugin_definition_type = is_object($derivative_definition) ? get_class($derivative_definition) : gettype($derivative_definition);
+      throw new \InvalidArgumentException(sprintf('Could not merge base plugin definitions of type %s into derivative plugin definition of type %s. Both must be arrays, or the derivative plugin definition must implement %s and the base plugin definition must implement %s.', $base_plugin_definition_type, $derivative_plugin_definition_type, MergeablePluginDefinitionInterface::class, PluginDefinitionInterface::class));
+    }
+  }
+
+  /**
+   * Sets the plugin ID on a plugin definition.
+   *
+   * @param mixed[]|\Drupal\Component\Plugin\Definition\PluginInspectionDefinitionInterface $plugin_definition
+   *   The plugin definition.
+   * @param string $plugin_id
+   *   The plugin ID to set.
+   */
+  protected function setPluginIdOnDefinition(&$plugin_definition, $plugin_id) {
+    if ($plugin_definition instanceof PluginInspectionDefinitionInterface) {
+      $plugin_definition->setId($plugin_id);
+    }
+    elseif (is_array($plugin_definition)) {
+      $plugin_definition['id'] = $plugin_id;
+    }
   }
 
   /**
diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
index 26c56f417a..c7664d4470 100644
--- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
+++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
@@ -981,7 +981,7 @@ public function getHighestId() {
       $migration_manager = $this->getMigrationPluginManager();
       $migrations = $migration_manager->getDefinitions();
       foreach ($migrations as $migration_id => $migration) {
-        if ($migration['id'] === $base_id) {
+        if ($base_id === substr($migration_id, 0, strpos($migration_id, $this::DERIVATIVE_SEPARATOR))) {
           // Get this derived migration's mapping table and add it to the list
           // of mapping tables to look in for the highest ID.
           $stub = $migration_manager->createInstance($migration_id);
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
index ef8d6c4790..1cf2ba1528 100644
--- a/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/MockBlockManager.php
@@ -55,7 +55,7 @@ public function __construct() {
     ]);
     // A plugin defining itself as a derivative.
     $this->discovery->setDefinition('menu:foo', [
-      'id' => 'menu',
+      'id' => 'menu:foo',
       'label' => t('Base label'),
       'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock',
     ]);
diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/PluginTestBase.php b/core/tests/Drupal/KernelTests/Core/Plugin/PluginTestBase.php
index a8df05585c..aed0b7330b 100644
--- a/core/tests/Drupal/KernelTests/Core/Plugin/PluginTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Plugin/PluginTestBase.php
@@ -24,7 +24,14 @@ abstract class PluginTestBase extends KernelTestBase {
 
   protected $testPluginManager;
   protected $testPluginExpectedDefinitions;
+
+  /**
+   * The mock plugin manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
   protected $mockBlockManager;
+
   protected $mockBlockExpectedDefinitions;
   protected $defaultsTestPluginManager;
   protected $defaultsTestPluginExpectedDefinitions;
@@ -63,17 +70,17 @@ protected function setUp() {
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock',
       ],
       'menu:main_menu' => [
-        'id' => 'menu',
+        'id' => 'menu:main_menu',
         'label' => 'Main menu',
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock',
       ],
       'menu:navigation' => [
-        'id' => 'menu',
+        'id' => 'menu:navigation',
         'label' => 'Navigation',
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock',
       ],
       'menu:foo' => [
-        'id' => 'menu',
+        'id' => 'menu:foo',
         'label' => 'Base label',
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock',
         'setting' => 'default',
@@ -84,7 +91,7 @@ protected function setUp() {
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock',
       ],
       'layout:foo' => [
-        'id' => 'layout',
+        'id' => 'layout:foo',
         'label' => 'Layout Foo',
         'class' => 'Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock',
       ],
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php b/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php
index 975e3e4ece..2e3ba965bc 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php
@@ -51,10 +51,10 @@ public function testGetDerivativeFetcher() {
 
     // Ensure that both test derivatives got added.
     $this->assertCount(2, $definitions);
-    $this->assertEquals('non_container_aware_discovery', $definitions['non_container_aware_discovery:test_discovery_0']['id']);
+    $this->assertEquals('non_container_aware_discovery:test_discovery_0', $definitions['non_container_aware_discovery:test_discovery_0']['id']);
     $this->assertEquals('\Drupal\Tests\Core\Plugin\Discovery\TestDerivativeDiscovery', $definitions['non_container_aware_discovery:test_discovery_0']['deriver']);
 
-    $this->assertEquals('non_container_aware_discovery', $definitions['non_container_aware_discovery:test_discovery_1']['id']);
+    $this->assertEquals('non_container_aware_discovery:test_discovery_1', $definitions['non_container_aware_discovery:test_discovery_1']['id']);
     $this->assertEquals('\Drupal\Tests\Core\Plugin\Discovery\TestDerivativeDiscovery', $definitions['non_container_aware_discovery:test_discovery_1']['deriver']);
   }
 
