diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index e6d7f243ac10ff9073eee2753421bfddb6c22064..4476e98d0c7efaecc72b038c75021f509f1947ba 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -240,6 +240,9 @@ config_dependencies_base:
       label: 'Configuration entity dependencies'
       sequence:
         type: string
+        constraints:
+          NotBlank: []
+          ConfigExists: []
     content:
       type: sequence
       label: 'Content entity dependencies'
@@ -250,11 +253,21 @@ config_dependencies_base:
       label: 'Module dependencies'
       sequence:
         type: string
+        constraints:
+          NotBlank: []
+          ExtensionName: []
+          ExtensionExists: module
     theme:
       type: sequence
       label: 'Theme dependencies'
       sequence:
         type: string
+        constraints:
+          NotBlank: []
+          ExtensionName: []
+          ExtensionExists: theme
+  constraints:
+    ValidKeys: '<infer>'
 
 config_dependencies:
   type: config_dependencies_base
@@ -263,6 +276,8 @@ config_dependencies:
     enforced:
       type: config_dependencies_base
       label: 'Enforced configuration dependencies'
+  constraints:
+    ValidKeys: '<infer>'
 
 config_entity:
   type: mapping
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..006cebd464d4ae084f23b0a7a3837902164f3a71
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraint.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that the value is the name of an existing config object.
+ *
+ * @Constraint(
+ *   id = "ConfigExists",
+ *   label = @Translation("Config exists", context = "Validation")
+ * )
+ */
+class ConfigExistsConstraint extends Constraint {
+
+  /**
+   * The error message.
+   *
+   * @var string
+   */
+  public string $message = "The '@name' config does not exist.";
+
+}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..590a3056a110eb7b56674ba1def000eae3d97826
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ConfigExistsConstraintValidator.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates that a given config object exists.
+ */
+class ConfigExistsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The config factory service.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected ConfigFactoryInterface $configFactory;
+
+  /**
+   * Constructs a ConfigExistsConstraintValidator object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('config.factory'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $name, Constraint $constraint) {
+    if (!in_array($name, $this->configFactory->listAll(), TRUE)) {
+      $this->context->addViolation($constraint->message, ['@name' => $name]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..39bb4e18417561915b56e90bd1999fec490579c9
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraint.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that config dependencies contain specific types of entities.
+ *
+ * @Constraint(
+ *   id = "RequiredConfigDependencies",
+ *   label = @Translation("Required config dependency types", context = "Validation")
+ * )
+ */
+class RequiredConfigDependenciesConstraint extends Constraint {
+
+  /**
+   * The error message.
+   *
+   * @var string
+   */
+  public string $message = 'This @entity_type requires a @dependency_type.';
+
+  /**
+   * The IDs of entity types that need to exist in config dependencies.
+   *
+   * For example, if an entity requires a filter format in its config
+   * dependencies, this should contain `filter_format`.
+   *
+   * @var string[]
+   */
+  public array $entityTypes = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['entityTypes'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'entityTypes';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..17802873cae67e69e98f7e8b5bfd7029a19d5887
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/RequiredConfigDependenciesConstraintValidator.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\LogicException;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the RequiredConfigDependencies constraint.
+ */
+class RequiredConfigDependenciesConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * Constructs a RequiredConfigDependenciesConstraintValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $entity, Constraint $constraint) {
+    assert($constraint instanceof RequiredConfigDependenciesConstraint);
+
+    // Only config entities can have config dependencies.
+    if (!$entity instanceof ConfigEntityInterface) {
+      throw new UnexpectedTypeException($entity, ConfigEntityInterface::class);
+    }
+
+    $config_dependencies = $entity->getDependencies()['config'] ?? [];
+
+    foreach ($constraint->entityTypes as $entity_type_id) {
+      $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+
+      if (!$entity_type instanceof ConfigEntityTypeInterface) {
+        throw new LogicException("'$entity_type_id' is not a config entity type.");
+      }
+
+      // Ensure the current entity type's config prefix is found in the config
+      // dependencies of the entity being validated.
+      $pattern = sprintf('/^%s\\.\\w+/', $entity_type->getConfigPrefix());
+      if (!preg_grep($pattern, $config_dependencies)) {
+        $this->context->addViolation($constraint->message, [
+          '@entity_type' => $entity->getEntityType()->getSingularLabel(),
+          '@dependency_type' => $entity_type->getSingularLabel(),
+        ]);
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..3432531564e94995d33fee7aab1938e0651812ad
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraint.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that the value is the name of an installed extension.
+ *
+ * @Constraint(
+ *   id = "ExtensionExists",
+ *   label = @Translation("Extension exists", context = "Validation")
+ * )
+ */
+class ExtensionExistsConstraint extends Constraint {
+
+  /**
+   * The error message for a non-existent module.
+   *
+   * @var string
+   */
+  public string $moduleMessage = "Module '@name' is not installed.";
+
+  /**
+   * The error message for a non-existent theme.
+   *
+   * @var string
+   */
+  public string $themeMessage = "Theme '@name' is not installed.";
+
+  /**
+   * The type of extension to look for. Can be 'module' or 'theme'.
+   *
+   * @var string
+   */
+  public string $type;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['type'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'type';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..404d5dcf38c220add9550b1e895daee2e1515856
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates that a given extension exists.
+ */
+class ExtensionExistsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected ModuleHandlerInterface $moduleHandler;
+
+  /**
+   * The theme handler service.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected ThemeHandlerInterface $themeHandler;
+
+  /**
+   * Constructs a ExtensionExistsConstraintValidator object.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handler service.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
+    $this->moduleHandler = $module_handler;
+    $this->themeHandler = $theme_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('module_handler'),
+      $container->get('theme_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $extension_name, Constraint $constraint) {
+    $variables = ['@name' => $extension_name];
+
+    switch ($constraint->type) {
+      case 'module':
+        if (!$this->moduleHandler->moduleExists($extension_name)) {
+          $this->context->addViolation($constraint->moduleMessage, $variables);
+        }
+        break;
+
+      case 'theme':
+        if (!$this->themeHandler->themeExists($extension_name)) {
+          $this->context->addViolation($constraint->themeMessage, $variables);
+        }
+        break;
+
+      default:
+        throw new \InvalidArgumentException("Unknown extension type: '$constraint->type'");
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..5721ba0bfd2c3076735e218ea2473333ca69a57c
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionNameConstraint.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Extension\Plugin\Validation\Constraint;
+
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Validation\Plugin\Validation\Constraint\RegexConstraint;
+
+/**
+ * Checks that the value is a valid extension name.
+ *
+ * @Constraint(
+ *   id = "ExtensionName",
+ *   label = @Translation("Valid extension name", context = "Validation")
+ * )
+ */
+class ExtensionNameConstraint extends RegexConstraint {
+
+  /**
+   * Constructs an ExtensionNameConstraint object.
+   *
+   * @param string|array|null $pattern
+   *   The regular expression to test for.
+   * @param mixed ...$arguments
+   *   Arguments to pass to the parent constructor.
+   */
+  public function __construct(string|array|null $pattern, ...$arguments) {
+    // Always use the regular expression that ExtensionDiscovery uses to find
+    // valid extensions.
+    $pattern = ExtensionDiscovery::PHP_FUNCTION_PATTERN;
+    parent::__construct($pattern, ...$arguments);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..842615c9ea2412fcc7685a568addc3a91b0cfb2d
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraint.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Schema\Mapping;
+use Drupal\Core\TypedData\MapDataDefinition;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Exception\InvalidArgumentException;
+
+/**
+ * Checks that all the keys of a mapping are known.
+ *
+ * @Constraint(
+ *   id = "ValidKeys",
+ *   label = @Translation("Valid mapping keys", context = "Validation"),
+ * )
+ */
+class ValidKeysConstraint extends Constraint {
+
+  /**
+   * The error message if an invalid key appears.
+   *
+   * @var string
+   */
+  public string $invalidKeyMessage = "'@key' is not a supported key.";
+
+  /**
+   * The error message if the array being validated is a list.
+   *
+   * @var string
+   */
+  public string $indexedArrayMessage = 'Numerically indexed arrays are not allowed.';
+
+  /**
+   * Keys which are allowed in the validated array, or `<infer>` to auto-detect.
+   *
+   * @var array|string
+   */
+  public array|string $allowedKeys;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'allowedKeys';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['allowedKeys'];
+  }
+
+  /**
+   * Returns the list of valid keys.
+   *
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The current execution context.
+   *
+   * @return string[]
+   *   The keys that will be considered valid.
+   */
+  public function getAllowedKeys(ExecutionContextInterface $context): array {
+    // If we were given an explicit array of allowed keys, return that.
+    if (is_array($this->allowedKeys)) {
+      return $this->allowedKeys;
+    }
+    // The only other value we'll accept is the string `<infer>`.
+    elseif ($this->allowedKeys === '<infer>') {
+      return static::inferKeys($context->getObject());
+    }
+    throw new InvalidArgumentException("'$this->allowedKeys' is not a valid set of allowed keys.");
+  }
+
+  /**
+   * Tries to auto-detect the schema-defined keys in a mapping.
+   *
+   * @param \Drupal\Core\Config\Schema\Mapping $mapping
+   *   The mapping to inspect.
+   *
+   * @return string[]
+   *   The keys defined in the mapping's schema.
+   */
+  protected static function inferKeys(Mapping $mapping): array {
+    $definition = $mapping->getDataDefinition();
+    assert($definition instanceof MapDataDefinition);
+
+    $definition = $definition->toArray();
+    assert(array_key_exists('mapping', $definition));
+    return array_keys($definition['mapping']);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..63562f5e80e91bac7abcdf77c343f226d791cadc
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the ValidKeys constraint.
+ */
+class ValidKeysConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    assert($constraint instanceof ValidKeysConstraint);
+
+    if (!is_array($value)) {
+      throw new UnexpectedTypeException($value, 'array');
+    }
+
+    // Indexed arrays are invalid by definition. array_is_list() returns TRUE
+    // for empty arrays, so only do this check if $value is not empty.
+    if ($value && array_is_list($value)) {
+      $this->context->addViolation($constraint->indexedArrayMessage);
+      return;
+    }
+
+    $invalid_keys = array_diff(
+      array_keys($value),
+      $constraint->getAllowedKeys($this->context)
+    );
+    foreach ($invalid_keys as $key) {
+      $this->context->addViolation($constraint->invalidKeyMessage, ['@key' => $key]);
+    }
+  }
+
+}
diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dd3683fd20c4beb8150c6252715c798dded4bc5d
--- /dev/null
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentTypeValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel;
+
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of block_content_type entities.
+ *
+ * @group block_content
+ */
+class BlockContentTypeValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['block_content'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = BlockContentType::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php b/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cc1891879c3efd9ce30550d286b5b43889a78c01
--- /dev/null
+++ b/core/modules/comment/tests/src/Kernel/CommentTypeValidationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\comment\Kernel;
+
+use Drupal\comment\Entity\CommentType;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of comment_type entities.
+ *
+ * @group comment
+ */
+class CommentTypeValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['comment', 'node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = CommentType::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'target_entity_type_id' => 'node',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php b/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b84b2ff626be18dd5f7123e8005dd27744d4b36f
--- /dev/null
+++ b/core/modules/contact/tests/src/Kernel/ContactFormValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\contact\Kernel;
+
+use Drupal\contact\Entity\ContactForm;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of contact_form entities.
+ *
+ * @group contact
+ */
+class ContactFormValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['contact', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = ContactForm::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/editor/src/Entity/Editor.php b/core/modules/editor/src/Entity/Editor.php
index 8b2d4fcea79680179ba2918dbab2e06f3abc06da..dd102d9179750dbfdd96ab574f9a7eeb86c602c1 100644
--- a/core/modules/editor/src/Entity/Editor.php
+++ b/core/modules/editor/src/Entity/Editor.php
@@ -30,6 +30,11 @@
  *     "editor",
  *     "settings",
  *     "image_upload",
+ *   },
+ *   constraints = {
+ *     "RequiredConfigDependencies" = {
+ *       "filter_format"
+ *     }
  *   }
  * )
  */
diff --git a/core/modules/editor/tests/src/Kernel/EditorValidationTest.php b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..696060ffbf6a99f2ac99347d97217f181c4e2cb6
--- /dev/null
+++ b/core/modules/editor/tests/src/Kernel/EditorValidationTest.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Tests\editor\Kernel;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of editor entities.
+ *
+ * @group editor
+ */
+class EditorValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['editor', 'editor_test', 'filter'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $format = FilterFormat::create([
+      'format' => 'test',
+      'name' => 'Test',
+    ]);
+    $format->save();
+
+    $this->entity = Editor::create([
+      'format' => $format->id(),
+      'editor' => 'unicorn',
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * Tests that validation fails if config dependencies are invalid.
+   */
+  public function testInvalidDependencies(): void {
+    // Remove the config dependencies from the editor entity.
+    $dependencies = $this->entity->getDependencies();
+    $dependencies['config'] = [];
+    $this->entity->set('dependencies', $dependencies);
+
+    $this->assertValidationErrors(['This text editor requires a text format.']);
+
+    // Things look sort-of like `filter.format.*` should fail validation
+    // because they don't exist.
+    $dependencies['config'] = [
+      'filter.format',
+      'filter.format.',
+    ];
+    $this->entity->set('dependencies', $dependencies);
+    $this->assertValidationErrors([
+      'This text editor requires a text format.',
+      "The 'filter.format' config does not exist.",
+      "The 'filter.format.' config does not exist.",
+    ]);
+  }
+
+}
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index 8067f5be5df062d33f243b893bfe4a96c49b5078..66d88deb2d3b7ba753a6c7007ec022160b7b8ee4 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -44,6 +44,11 @@
  *     "default_value_callback",
  *     "settings",
  *     "field_type",
+ *   },
+ *   constraints = {
+ *     "RequiredConfigDependencies" = {
+ *       "field_storage_config"
+ *     }
  *   }
  * )
  */
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee5cc5243757c496fe84ee98cc3b546797a535c8
--- /dev/null
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\Tests\field\Kernel\Entity;
+
+use Drupal\field\Entity\FieldConfig;
+
+/**
+ * Tests validation of field_config entities.
+ *
+ * @group field
+ */
+class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // The field storage was created in the parent method.
+    $field_storage = $this->entity;
+
+    $this->entity = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * Tests that validation fails if config dependencies are invalid.
+   */
+  public function testInvalidDependencies(): void {
+    // Remove the config dependencies from the field entity.
+    $dependencies = $this->entity->getDependencies();
+    $dependencies['config'] = [];
+    $this->entity->set('dependencies', $dependencies);
+
+    $this->assertValidationErrors(['This field requires a field storage.']);
+
+    // Things look sort-of like `field.storage.*.*` should fail validation
+    // because they don't exist.
+    $dependencies['config'] = [
+      'field.storage.fake',
+      'field.storage.',
+      'field.storage.user.',
+    ];
+    $this->entity->set('dependencies', $dependencies);
+    $this->assertValidationErrors([
+      "The 'field.storage.fake' config does not exist.",
+      "The 'field.storage.' config does not exist.",
+      "The 'field.storage.user.' config does not exist.",
+    ]);
+  }
+
+}
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..95ba5ec42bd8d4ec323bcaf865daa3afb6238f72
--- /dev/null
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\field\Kernel\Entity;
+
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of field_storage_config entities.
+ *
+ * @group field
+ */
+class FieldStorageConfigValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['field', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'test',
+      'entity_type' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php b/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0c8fb27ec440957906437dbcecbf63b932a07f4a
--- /dev/null
+++ b/core/modules/filter/tests/src/Kernel/FilterFormatValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\filter\Kernel;
+
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of filter_format entities.
+ *
+ * @group filter
+ */
+class FilterFormatValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['filter'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = FilterFormat::create([
+      'format' => 'test',
+      'name' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php b/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..65e9288c449f1d87e9548d72b009b417639657a6
--- /dev/null
+++ b/core/modules/image/tests/src/Kernel/ImageStyleValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\image\Kernel;
+
+use Drupal\image\Entity\ImageStyle;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of image_style entities.
+ *
+ * @group image
+ */
+class ImageStyleValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['image'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = ImageStyle::create([
+      'name' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php b/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c95b3163cd07b5927de03f0d0bf5a4e4f53e39e
--- /dev/null
+++ b/core/modules/language/tests/src/Kernel/ConfigurableLanguageValidationTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Tests\language\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests validation of configurable_language entities.
+ *
+ * @group language
+ */
+class ConfigurableLanguageValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['language'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = ConfigurableLanguage::createFromLangcode('fr');
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..01a2e6a77ed9782ff00016327b0579a673357125
--- /dev/null
+++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\language\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\language\Entity\ContentLanguageSettings;
+
+/**
+ * Tests validation of content_language_settings entities.
+ *
+ * @group language
+ */
+class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['language', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = ContentLanguageSettings::create([
+      'target_entity_type_id' => 'user',
+      'target_bundle' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0c6032afb6a5550b16827326e0d86ac82d8ed6f0
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Kernel;
+
+use Drupal\Core\Entity\Entity\EntityViewMode;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+
+/**
+ * Tests validation of Layout Builder's entity_view_display entities.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['layout_builder', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    EntityViewMode::create([
+      'id' => 'user.layout',
+      'label' => 'Layout',
+      'targetEntityType' => 'user',
+    ])->save();
+
+    $this->entity = LayoutBuilderEntityViewDisplay::create([
+      'mode' => 'layout',
+      'label' => 'Layout',
+      'targetEntityType' => 'user',
+      'bundle' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5f2774b2c896a7473a8ce570b0cbcd43491c100
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Tests\media\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+
+/**
+ * Tests validation of media_type entities.
+ *
+ * @group media
+ */
+class MediaTypeValidationTest extends ConfigEntityValidationTestBase {
+
+  use MediaTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['field', 'media', 'media_test_source'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->entity = $this->createMediaType('test');
+  }
+
+}
diff --git a/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php b/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5bcc47355e640dacae2516e98c20d8c582da1ff
--- /dev/null
+++ b/core/modules/node/tests/src/Kernel/NodeTypeValidationTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\Tests\node\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+
+/**
+ * Tests validation of node_type entities.
+ *
+ * @group node
+ */
+class NodeTypeValidationTest extends ConfigEntityValidationTestBase {
+
+  use ContentTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['field', 'node', 'text', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('node');
+    $this->entity = $this->createContentType();
+  }
+
+}
diff --git a/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php b/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c9a4cb98c92329f86eb6ee2b4fe7a137ff3fe49
--- /dev/null
+++ b/core/modules/responsive_image/tests/src/Kernel/ResponsiveImageStyleValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\responsive_image\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\responsive_image\Entity\ResponsiveImageStyle;
+
+/**
+ * Tests validation of responsive_image_style entities.
+ *
+ * @group responsive_image
+ */
+class ResponsiveImageStyleValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['breakpoint', 'image', 'responsive_image'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = ResponsiveImageStyle::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d8cb96272cbc572abfd55b3b39b555dbc94ffe7
--- /dev/null
+++ b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\rest\Kernel\Entity;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\rest\Entity\RestResourceConfig;
+use Drupal\rest\RestResourceConfigInterface;
+
+/**
+ * Tests validation of rest_resource_config entities.
+ *
+ * @group rest
+ */
+class RestResourceConfigValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['rest', 'serialization'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = RestResourceConfig::create([
+      'id' => 'test',
+      'plugin_id' => 'entity:date_format',
+      'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
+      'configuration' => [],
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb35264fd3f1b373aa8c43501f75726d92521187
--- /dev/null
+++ b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\search\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\search\Entity\SearchPage;
+
+/**
+ * Tests validation of search_page entities.
+ *
+ * @group search
+ */
+class SearchPageValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['search', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = SearchPage::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'plugin' => 'user_search',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a27f3980d18658214687c2c138c8ace96d034e5d
--- /dev/null
+++ b/core/modules/shortcut/tests/src/Kernel/ShortcutSetValidationTest.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\Tests\shortcut\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\shortcut\Entity\ShortcutSet;
+
+/**
+ * Tests validation of shortcut_set entities.
+ *
+ * @group shortcut
+ */
+class ShortcutSetValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['link', 'shortcut'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('shortcut');
+    $this->installEntitySchema('shortcut');
+
+    $this->entity = ShortcutSet::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..133d1df18977f43672d48eeee2ba9b790839043b
--- /dev/null
+++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Tests\system\Kernel\Entity;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\system\Entity\Action;
+
+/**
+ * Tests validation of action entities.
+ *
+ * @group system
+ */
+class ActionValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Action::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'type' => 'test',
+      'plugin' => 'action_goto_action',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c1ebd752b680e1708cc1fa3ccb01e9c148b92c5
--- /dev/null
+++ b/core/modules/system/tests/src/Kernel/Entity/MenuValidationTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Tests\system\Kernel\Entity;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\system\Entity\Menu;
+
+/**
+ * Tests validation of menu entities.
+ *
+ * @group system
+ */
+class MenuValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Menu::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7de121d2dffe87efbf82e685cc9320634e093f8c
--- /dev/null
+++ b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\taxonomy\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\taxonomy\Entity\Vocabulary;
+
+/**
+ * Tests validation of vocabulary entities.
+ *
+ * @group taxonomy
+ */
+class VocabularyValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['taxonomy'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Vocabulary::create([
+      'vid' => 'test',
+      'name' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/user/tests/src/Kernel/RoleValidationTest.php b/core/modules/user/tests/src/Kernel/RoleValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff78efd37d1c976ec92f0e2c9f39afcaf7860c63
--- /dev/null
+++ b/core/modules/user/tests/src/Kernel/RoleValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\user\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\user\Entity\Role;
+
+/**
+ * Tests validation of user_role entities.
+ *
+ * @group user
+ */
+class RoleValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Role::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..94bc6023fc8bd6e2d67d3b7eb9207c14849fbaa3
--- /dev/null
+++ b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\views\Kernel\Entity;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\views\Entity\View;
+
+/**
+ * Tests validation of view entities.
+ *
+ * @group views
+ */
+class ViewValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['views'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = View::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a3448a1a10cad69564d79631d9e3c477503a2710
--- /dev/null
+++ b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\workflows\Kernel;
+
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests validation of workflow entities.
+ *
+ * @group workflows
+ */
+class WorkflowValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['workflows', 'workflow_type_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = Workflow::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'type' => 'workflow_type_test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..e3dcdffc6423d98b34c25721ba0b393fb7235ecd
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Config;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Base class for testing validation of config entities.
+ *
+ * @group config
+ * @group Validation
+ */
+abstract class ConfigEntityValidationTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * The config entity being tested.
+   *
+   * @var \Drupal\Core\Config\Entity\ConfigEntityInterface
+   */
+  protected ConfigEntityInterface $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('system');
+
+    // Install Stark so we can add a legitimately installed theme to config
+    // dependencies.
+    $this->container->get('theme_installer')->install(['stark']);
+    $this->container = $this->container->get('kernel')->getContainer();
+  }
+
+  /**
+   * Data provider for ::testConfigDependenciesValidation().
+   *
+   * @return array[]
+   *   The test cases.
+   */
+  public function providerConfigDependenciesValidation(): array {
+    return [
+      'valid dependency types' => [
+        [
+          'config' => ['system.site'],
+          'content' => ['node:some-random-uuid'],
+          'module' => ['system'],
+          'theme' => ['stark'],
+        ],
+        [],
+      ],
+      'unknown dependency type' => [
+        [
+          'fun_stuff' => ['star-trek.deep-space-nine'],
+        ],
+        [
+          "'fun_stuff' is not a supported key.",
+        ],
+      ],
+      'empty string in config dependencies' => [
+        [
+          'config' => [''],
+        ],
+        [
+          'This value should not be blank.',
+          "The '' config does not exist.",
+        ],
+      ],
+      'non-existent config dependency' => [
+        [
+          'config' => ['fake_settings'],
+        ],
+        [
+          "The 'fake_settings' config does not exist.",
+        ],
+      ],
+      'empty string in module dependencies' => [
+        [
+          'module' => [''],
+        ],
+        [
+          'This value should not be blank.',
+          "Module '' is not installed.",
+        ],
+      ],
+      'invalid module dependency' => [
+        [
+          'module' => ['invalid-module-name'],
+        ],
+        [
+          'This value is not valid.',
+          "Module 'invalid-module-name' is not installed.",
+        ],
+      ],
+      'non-installed module dependency' => [
+        [
+          'module' => ['bad_judgment'],
+        ],
+        [
+          "Module 'bad_judgment' is not installed.",
+        ],
+      ],
+      'empty string in theme dependencies' => [
+        [
+          'theme' => [''],
+        ],
+        [
+          'This value should not be blank.',
+          "Theme '' is not installed.",
+        ],
+      ],
+      'invalid theme dependency' => [
+        [
+          'theme' => ['invalid-theme-name'],
+        ],
+        [
+          'This value is not valid.',
+          "Theme 'invalid-theme-name' is not installed.",
+        ],
+      ],
+      'non-installed theme dependency' => [
+        [
+          'theme' => ['ugly_theme'],
+        ],
+        [
+          "Theme 'ugly_theme' is not installed.",
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests validation of config dependencies.
+   *
+   * @param array[] $dependencies
+   *   The dependencies that should be added to the config entity under test.
+   * @param string[] $expected_messages
+   *   The expected constraint violation messages.
+   *
+   * @dataProvider providerConfigDependenciesValidation
+   */
+  public function testConfigDependenciesValidation(array $dependencies, array $expected_messages): void {
+    $this->assertInstanceOf(ConfigEntityInterface::class, $this->entity);
+
+    // The entity should have valid data to begin with.
+    $this->assertValidationErrors([]);
+
+    // Add the dependencies we were given to the dependencies that may already
+    // exist in the entity.
+    $dependencies = NestedArray::mergeDeep($this->entity->getDependencies(), $dependencies);
+
+    $this->entity->set('dependencies', $dependencies);
+    $this->assertValidationErrors($expected_messages);
+
+    // Enforce these dependencies, and ensure we get the same results.
+    $this->entity->set('dependencies', [
+      'enforced' => $dependencies,
+    ]);
+    $this->assertValidationErrors($expected_messages);
+  }
+
+  /**
+   * Asserts a set of validation errors is raised when the entity is validated.
+   *
+   * @param string[] $expected_messages
+   *   The expected validation error messages.
+   */
+  protected function assertValidationErrors(array $expected_messages): void {
+    /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
+    $typed_data = $this->container->get('typed_data_manager');
+    $definition = $typed_data->createDataDefinition('entity:' . $this->entity->getEntityTypeId());
+    $violations = $typed_data->create($definition, $this->entity)->validate();
+
+    $actual_messages = [];
+    foreach ($violations as $violation) {
+      $actual_messages[] = (string) $violation->getMessage();
+    }
+    $this->assertSame($expected_messages, $actual_messages);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..03992fd9c92180d5906a921169657de3198526d1
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Config;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the ConfigExists constraint validator.
+ *
+ * @group config
+ * @group Validation
+ *
+ * @covers \Drupal\Core\Config\Plugin\Validation\Constraint\ConfigExistsConstraint
+ * @covers \Drupal\Core\Config\Plugin\Validation\Constraint\ConfigExistsConstraintValidator
+ */
+class ConfigExistsConstraintValidatorTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Tests the ConfigExists constraint validator.
+   */
+  public function testValidation(): void {
+    // Create a data definition that specifies the value must be a string with
+    // the name of an existing piece of config.
+    $definition = DataDefinition::create('string')
+      ->addConstraint('ConfigExists');
+
+    /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
+    $typed_data = $this->container->get('typed_data_manager');
+    $data = $typed_data->create($definition, 'system.site');
+
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'system.site' config does not exist.", (string) $violations->get(0)->getMessage());
+
+    $this->installConfig('system');
+    $this->assertCount(0, $data->validate());
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e13c50d0eeaf2e2c416c900c1e36aeda752b2af8
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of base_field_override entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $fields = $this->container->get('entity_field.manager')
+      ->getBaseFieldDefinitions('user');
+
+    $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'user');
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..10e97012bbdb8b35cfb76a0a43dcb6688d444588
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/DateFormatValidationTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of date_format entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class DateFormatValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = DateFormat::create([
+      'id' => 'test',
+      'label' => 'Test',
+      'pattern' => 'Y-m-d',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cda1760508277b3a1b871aac42a5c340a3ca9180
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+
+/**
+ * Tests validation of entity_form_display entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class EntityFormDisplayValidationTest extends EntityFormModeValidationTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = EntityFormDisplay::create([
+      'label' => 'Test',
+      'targetEntityType' => 'user',
+      'bundle' => 'user',
+      // The mode was created by the parent class.
+      'mode' => 'test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..31e31249bb20336d27b2d279ebeba6c469e711f7
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Entity\EntityFormMode;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of entity_form_mode entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class EntityFormModeValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installConfig('user');
+
+    $this->entity = EntityFormMode::create([
+      'id' => 'user.test',
+      'label' => 'Test',
+      'targetEntityType' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b30998db1ac083a23eb47860bc975700514ee7aa
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
+/**
+ * Tests validation of entity_view_display entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class EntityViewDisplayValidationTest extends EntityViewModeValidationTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = EntityViewDisplay::create([
+      'label' => 'Test',
+      'targetEntityType' => 'user',
+      'bundle' => 'user',
+      // The mode was created by the parent class.
+      'mode' => 'test',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..011a5e4844cc2275e9a8c6c68dea0234dbfb3a97
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewMode;
+use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
+
+/**
+ * Tests validation of entity_view_mode entities.
+ *
+ * @group Entity
+ * @group Validation
+ */
+class EntityViewModeValidationTest extends ConfigEntityValidationTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->installConfig('user');
+
+    $this->entity = EntityViewMode::create([
+      'id' => 'user.test',
+      'label' => 'Test',
+      'targetEntityType' => 'user',
+    ]);
+    $this->entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f698279a56d6d1287f8883d234428bf61d6f4f46
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the ExtensionExists constraint validator.
+ *
+ * @group Validation
+ *
+ * @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraint
+ * @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionExistsConstraintValidator
+ */
+class ExtensionExistsConstraintValidatorTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Tests the ExtensionExists constraint validator.
+   */
+  public function testValidation(): void {
+    // Create a data definition that specifies the value must be a string with
+    // the name of an installed module.
+    $definition = DataDefinition::create('string')
+      ->addConstraint('ExtensionExists', 'module');
+
+    /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
+    $typed_data = $this->container->get('typed_data_manager');
+    $data = $typed_data->create($definition, 'user');
+
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("Module 'user' is not installed.", (string) $violations->get(0)->getMessage());
+
+    $this->enableModules(['user']);
+    $this->assertCount(0, $data->validate());
+
+    $definition->setConstraints(['ExtensionExists' => 'theme']);
+    $data = $typed_data->create($definition, 'stark');
+
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("Theme 'stark' is not installed.", (string) $violations->get(0)->getMessage());
+
+    $this->assertTrue($this->container->get('theme_installer')->install(['stark']));
+    // Installing the theme rebuilds the container, so we need to ensure the
+    // constraint is instantiated with an up-to-date theme handler.
+    $data = $this->container->get('kernel')
+      ->getContainer()
+      ->get('typed_data_manager')
+      ->create($definition, 'stark');
+    $this->assertCount(0, $data->validate());
+
+    // Anything but a module or theme should raise an exception.
+    $definition->setConstraints(['ExtensionExists' => 'profile']);
+    $this->expectExceptionMessage("Unknown extension type: 'profile'");
+    $data->validate();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ee79e95c16df481a8f6303de3a704dd34d45726
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the ExtensionName constraint.
+ *
+ * @group Validation
+ *
+ * @covers \Drupal\Core\Extension\Plugin\Validation\Constraint\ExtensionNameConstraint
+ */
+class ExtensionNameConstraintTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Tests the ExtensionName constraint.
+   */
+  public function testValidation(): void {
+    // Create a data definition that specifies the value must be a string with
+    // the name of a valid extension.
+    $definition = DataDefinition::create('string')
+      ->addConstraint('ExtensionName');
+
+    /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
+    $typed_data = $this->container->get('typed_data_manager');
+    $data = $typed_data->create($definition, 'user');
+
+    $this->assertCount(0, $data->validate());
+
+    $data->setValue('invalid-name');
+    $violations = $data->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame('This value is not valid.', (string) $violations->get(0)->getMessage());
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e3877465e59cc5c4973fd4ccc77ea55ddcb9a94e
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/TypedData/ValidKeysConstraintValidatorTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\KernelTests\Core\TypedData;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Tests the ValidKeys validation constraint.
+ *
+ * @group Validation
+ *
+ * @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint
+ * @covers \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraintValidator
+ */
+class ValidKeysConstraintValidatorTest extends KernelTestBase {
+
+  /**
+   * Tests the ValidKeys constraint validator.
+   */
+  public function testValidation(): void {
+    // Create a data definition that specifies certain allowed keys.
+    $definition = DataDefinition::create('any')
+      ->addConstraint('ValidKeys', ['north', 'south', 'west']);
+
+    /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
+    $typed_data = $this->container->get('typed_data_manager');
+
+    // Passing a non-array value should raise an exception.
+    try {
+      $typed_data->create($definition, 2501)->validate();
+      $this->fail('Expected an exception but none was raised.');
+    }
+    catch (UnexpectedTypeException $e) {
+      $this->assertSame('Expected argument of type "array", "int" given', $e->getMessage());
+    }
+
+    // Empty arrays are valid.
+    $this->assertCount(0, $typed_data->create($definition, [])->validate());
+
+    // Indexed arrays are never valid.
+    $violations = $typed_data->create($definition, ['north', 'south'])->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame('Numerically indexed arrays are not allowed.', (string) $violations->get(0)->getMessage());
+
+    // Arrays with automatically assigned keys, AND a valid key, should be
+    // considered invalid overall.
+    $violations = $typed_data->create($definition, ['north', 'south' => 'west'])->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("'0' is not a supported key.", (string) $violations->get(0)->getMessage());
+
+    // Associative arrays with an invalid key should be invalid.
+    $violations = $typed_data->create($definition, ['north' => 'south', 'east' => 'west'])->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("'east' is not a supported key.", (string) $violations->get(0)->getMessage());
+
+    // If the array only contains the allowed keys, it's fine.
+    $value = [
+      'north' => 'Boston',
+      'south' => 'Atlanta',
+      'west' => 'San Francisco',
+    ];
+    $violations = $typed_data->create($definition, $value)->validate();
+    $this->assertCount(0, $violations);
+  }
+
+  /**
+   * Tests that valid keys can be inferred from the data definition.
+   */
+  public function testValidKeyInference(): void {
+    // Install the System module and its config so that we can test that the
+    // validator infers the allowed keys from a defined schema.
+    $this->enableModules(['system']);
+    $this->installConfig('system');
+
+    $config = $this->container->get('config.typed')
+      ->get('system.site');
+    $config->getDataDefinition()
+      ->addConstraint('ValidKeys', '<infer>');
+
+    $data = $config->getValue();
+    $data['invalid-key'] = "There's a snake in my boots.";
+    $config->setValue($data);
+    $violations = $config->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("'invalid-key' is not a supported key.", (string) $violations->get(0)->getMessage());
+
+    // Ensure that ValidKeys will freak out if the option is not exactly
+    // `<infer>`.
+    $config->getDataDefinition()
+      ->addConstraint('ValidKeys', 'infer');
+    $this->expectExceptionMessage("'infer' is not a valid set of allowed keys.");
+    $config->validate();
+  }
+
+}