diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index b7798d0373d7023a4a23eb2eff11f936b723f650..21a43e8dbb9a3d51c6bd6ca4a05448fb60dfedb6 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -533,16 +533,29 @@ base_entity_reference_field_settings:
 
 field_config_base:
   type: config_entity
+  constraints:
+    UniqueFieldCombination: [entity_type, bundle, field_name]
   mapping:
     id:
       type: string
       label: 'ID'
+      constraints:
+        StringParts:
+          separator: .
+          parts:
+            - '%parent.entity_type'
+            - '%parent.bundle'
+            - '%parent.field_name'
     field_name:
       type: string
       label: 'Field name'
     entity_type:
       type: string
       label: 'Entity type'
+      constraints:
+        PluginExists:
+          manager: entity_type.manager
+          interface: \Drupal\Core\Entity\FieldableEntityInterface
     bundle:
       type: string
       label: 'Bundle'
@@ -554,6 +567,7 @@ field_config_base:
     description:
       type: text
       label: 'Help text'
+      nullable: true
     required:
       type: boolean
       label: 'Required field'
@@ -563,14 +577,20 @@ field_config_base:
     default_value:
       type: sequence
       label: 'Default values'
+      nullable: true
       sequence:
         type: field.value.[%parent.%parent.field_type]
         label: 'Default value'
+        constraint:
+          # TBD, looks like \Symfony\Component\Validator\Constraints\ValidValidator *could* work here…
+          Valid: []
     default_value_callback:
       type: string
       label: 'Default value callback'
+      # @todo Constraints — validate that this callback A) returns an entity, B) a *valid* entity?
     settings:
       type: field.field_settings.[%parent.field_type]
+      # @todo Constraints on each of the field type-specific settings config schemas BUT extra special challenge: until Drupal 11, at best only core field types will have validation constraints set!
     field_type:
       type: string
       label: 'Field type'
@@ -582,6 +602,8 @@ field_config_base:
 core.base_field_override.*.*.*:
   type: field_config_base
   label: 'Base field bundle override'
+  constraints:
+    FullyValidatable: ~
 
 core.date_format.*:
   type: config_entity
@@ -666,6 +688,12 @@ field.storage_settings.string:
       type: boolean
       label: 'Contains US ASCII characters only'
 
+field.field_settings.string:
+  type: mapping
+  label: 'String settings'
+  constraints:
+    FullyValidatable: ~
+
 field.value.string:
   type: mapping
   label: 'Default value'
@@ -683,6 +711,14 @@ field.storage_settings.string_long:
     case_sensitive:
       type: boolean
       label: 'Case sensitive'
+  constraints:
+    FullyValidatable: ~
+
+field.field_settings.string_long:
+  type: mapping
+  label: 'String (long) settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.string_long:
   type: mapping
@@ -698,6 +734,14 @@ field.value.string_long:
 field.storage_settings.password:
   type: field.storage_settings.string
   label: 'Password settings'
+  constraints:
+    FullyValidatable: ~
+
+field.field_settings.password:
+  type: mapping
+  label: 'Password settings'
+  constraints:
+    FullyValidatable: ~
 
 # Schema for the configuration of the URI field type.
 # This field type has no field instance settings, so no specific config schema type.
@@ -713,6 +757,12 @@ field.storage_settings.uri:
       type: boolean
       label: 'Case sensitive'
 
+field.field_settings.uri:
+  type: mapping
+  label: 'URI settings'
+  constraints:
+    FullyValidatable: ~
+
 field.value.uri:
   type: mapping
   label: 'Default value'
@@ -722,10 +772,19 @@ field.value.uri:
       label: 'Value'
 
 # Schema for the configuration of the Created field type.
-# This field type has no field storage settings, so no specific config schema type.
-# @see `type: field.storage_settings.*`
-# This field type has no field instance settings, so no specific config schema type.
-# @see `type: field.field_settings.*`
+
+field.storage_settings.created:
+  type: mapping
+  label: 'Created timestamp settings'
+  constraints:
+    FullyValidatable: ~
+
+field.field_settings.created:
+  type: mapping
+  label: 'Created timestamp settings'
+  constraints:
+    FullyValidatable: ~
+
 field.value.created:
   type: mapping
   label: 'Default value'
@@ -735,10 +794,19 @@ field.value.created:
       label: 'Value'
 
 # Schema for the configuration of the Changed field type.
-# This field type has no field storage settings, so no specific config schema type.
-# @see `type: field.storage_settings.*`
-# This field type has no field instance settings, so no specific config schema type.
-# @see `type: field.field_settings.*`
+
+field.storage_settings.changed:
+  type: mapping
+  label: 'Changed timestamp settings'
+  constraints:
+    FullyValidatable: ~
+
+field.field_settings.changed:
+  type: mapping
+  label: 'Changed timestamp settings'
+  constraints:
+    FullyValidatable: ~
+
 field.value.changed:
   type: mapping
   label: 'Default value'
@@ -756,6 +824,9 @@ field.storage_settings.entity_reference:
     target_type:
       type: string
       label: 'Type of item to reference'
+      constraints:
+        PluginExists:
+          manager: entity_type.manager
 
 field.field_settings.entity_reference:
   type: mapping
@@ -797,6 +868,8 @@ field.field_settings.boolean:
     off_label:
       type: required_label
       label: 'Off label'
+  constraints:
+    FullyValidatable: ~
 
 field.value.boolean:
   type: mapping
@@ -806,8 +879,13 @@ field.value.boolean:
       label: 'Value'
 
 # Schema for the configuration of the Email field type.
-# This field type has no field storage settings, so no specific config schema type.
-# @see `type: field.storage_settings.*`
+
+field.storage_settings.email:
+  type: mapping
+  label: 'Email settings'
+  constraints:
+    FullyValidatable: ~
+
 field.field_settings.email:
   type: mapping
   label: 'Email settings'
@@ -904,6 +982,8 @@ field.value.decimal:
 field.storage_settings.float:
   type: mapping
   label: 'Float settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.float:
   type: mapping
diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php
index 39405c37d437dae592076ecb5ce9a65ba96dd26b..3d8f166e8baf7c98c9e223da37e11569623a78fa 100644
--- a/core/lib/Drupal/Core/Field/FieldConfigBase.php
+++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
@@ -81,9 +81,9 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn
    * For example, the description will be the help text of Form API elements for
    * this field in entity edit forms.
    *
-   * @var string
+   * @var string|null
    */
-  protected $description = '';
+  protected $description = NULL;
 
   /**
    * Field-type specific settings.
@@ -338,7 +338,7 @@ public function setLabel($label) {
    * {@inheritdoc}
    */
   public function getDescription() {
-    return $this->description;
+    return $this->description ?? '';
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4a1e1cc5e42653f488bd78b09e0acd4ac4a5cc0
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Validates that the value overrides a base field that actually exists.
+ *
+ * @Constraint(
+ *   id = "IsBaseField",
+ *   label = @Translation("Is a base field", context = "Validation")
+ * )
+ */
+class IsBaseFieldConstraint extends Constraint {
+
+  /**
+   * Tbe error message if validation fails.
+   *
+   * @var string
+   */
+  public string $message = "'@field_name' is not a base field of the @entity_type entity type.";
+
+}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..be309120e5f95b059e52ed0483cc1b07e28e4e33
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the IsBaseField constraint.
+ */
+class IsBaseFieldConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * Constructs an IsBaseFieldConstraintValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
+   *   The entity field manager service.
+   */
+  public function __construct(protected readonly EntityFieldManagerInterface $entityFieldManager) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container): static {
+    return new static(
+      $container->get(EntityFieldManagerInterface::class),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint): void {
+    assert($constraint instanceof IsBaseFieldConstraint);
+
+    if (!$value instanceof FieldDefinitionInterface) {
+      throw new UnexpectedTypeException($value, FieldDefinitionInterface::class);
+    }
+    $field_name = $value->getName();
+    $entity_type_id = $value->getTargetEntityTypeId();
+    if (!array_key_exists($field_name, $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id))) {
+      $this->context->addViolation($constraint->message, [
+        '@field_name' => $field_name,
+        '@entity_type' => $entity_type_id,
+      ]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..d3007bf07b9be23a61618959beed60403b625e83
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that a value in this config matches a value in other config.
+ *
+ * @Constraint(
+ *   id = "MatchesOtherConfigValue",
+ *   label = @Translation("Value must match value in other config", context = "Validation")
+ * )
+ */
+class MatchesOtherConfigValueConstraint extends Constraint {
+
+  /**
+   * The error message if the string does not match.
+   *
+   * @var string
+   */
+  public string $message = "Expected this to match the value in the '@other_config_name' config at the '@other_config_property_path' property: '@expected_value' was expected, not '@actual_value'.";
+
+  /**
+   * The config prefix to use.
+   *
+   * @var string
+   */
+  public string $prefix;
+
+  /**
+   * One or more expressions that resolve to the config entity ID parts.
+   *
+   * @var string|string[]
+   * @see \Drupal\Core\Config\Schema\TypeResolver::resolveExpression()
+   */
+  public string|array $id;
+
+  /**
+   * The property path.
+   *
+   * @var string
+   */
+  public string $propertyPath;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions(): array {
+    return ['prefix', 'id', 'propertyPath'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..fc1efcb38e25808420a2ac4f04b3376f65f15e6b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
@@ -0,0 +1,121 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\Schema\TypeResolver;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\TypedData\PrimitiveInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the MatchesOtherConfigValue constraint.
+ */
+class MatchesOtherConfigValueConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * Constructs a MatchesOtherConfigValueConstraintValidator object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory service.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
+   */
+  public function __construct(
+    protected readonly ConfigFactoryInterface $configFactory,
+    protected readonly TypedConfigManagerInterface $typedConfigManager,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container): static {
+    return new static(
+      $container->get(ConfigFactoryInterface::class),
+      $container->get(TypedConfigManagerInterface::class)
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    if (!$constraint instanceof MatchesOtherConfigValueConstraint) {
+      throw new UnexpectedTypeException($constraint, MatchesOtherConfigValueConstraint::class);
+    }
+
+    $this_property = $this->context->getObject();
+    assert($this_property instanceof TypedDataInterface);
+    $this_data_definition = $this_property->getDataDefinition();
+
+    // This validation constraint should only be used for scalar values
+    // (primitives()), not for comparisons across entire arrays (sequences or
+    // mappings). That is possible in theory but not in practice yet.
+    // @todo Re-evaluate this after https://www.drupal.org/project/drupal/issues/3230826
+    if (!is_subclass_of($this_data_definition->getClass(), PrimitiveInterface::class)) {
+      throw new \LogicException(sprintf(
+        'The MatchesOtherConfigValue constraint used at %s uses the %s config schema type. Only config schema types implementing %s are supported.',
+        $this_property->getPropertyPath(),
+        $this_data_definition->getDataType(),
+        PrimitiveInterface::class,
+      ));
+    }
+
+    // Determine the name of the other config object.
+    $id_parts = array_map(
+      fn (string $expression): mixed => TypeResolver::resolveExpression($expression, $this_property),
+      (array) $constraint->id
+    );
+    $other_config_name = $constraint->prefix . implode('.', $id_parts);
+
+    // When a developer uses this constraint inappropriately, guide them.
+    if ($other_config_name === $this->context->getRoot()->getName()) {
+      throw new \LogicException(sprintf('The MatchesOtherConfigValue constraint used in %s is configured to not look at another config object but at itself.', $this_property->getPropertyPath()));
+    }
+    if (!in_array($other_config_name, $this->configFactory->listAll(), TRUE)) {
+      throw new \LogicException(sprintf('The config %s does not exist, and is assumed to exist by this constraint. This means a RequiredConfigDependencies is absent from the config entity type.', $other_config_name));
+    }
+
+    $other_config_object = $this->typedConfigManager->get($other_config_name);
+    $other_property_to_match = $other_config_object->get($constraint->propertyPath);
+
+    // When a developer uses this constraint inappropriately, guide them.
+    $other_data_definition = $other_property_to_match->getDataDefinition();
+    if ($this_data_definition->getDataType() !== $other_data_definition->getDataType()) {
+      throw new \LogicException(sprintf(
+        'The config schema type of this value (%s) does not match the config schema type (%s) of the %s property in the %s config object.',
+        $this_data_definition->getDataType(),
+        $other_data_definition->getDataType(),
+        $constraint->propertyPath,
+        $other_config_name,
+      ));
+    }
+    $missing_constraints = array_diff_key($other_data_definition->getConstraints(), $this_data_definition->getConstraints());
+    if (!empty($missing_constraints)) {
+      throw new \LogicException(sprintf(
+        'Fewer constraints are applied to this value than to the %s property in the %s config object. The following are missing: %s.',
+        $constraint->propertyPath,
+        $other_config_name,
+        implode(', ', array_keys($missing_constraints)),
+      ));
+    }
+
+    assert($value === $this_property->getValue());
+    if ($value !== $other_property_to_match->getValue()) {
+      $this->context->addViolation($constraint->message, [
+        '@other_config_name' => $other_config_name,
+        '@other_config_property_path' => $constraint->propertyPath,
+        '@expected_value' => $other_property_to_match->getString(),
+        '@actual_value' => $this_property->getString(),
+      ]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..59cda8e8f50e547ea768783ccc76d266f8b8d1df
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks a string consists of specific parts found in the parent mapping.
+ *
+ * @Constraint(
+ *   id = "StringParts",
+ *   label = @Translation("String consists of specific parts", context = "Validation")
+ * )
+ */
+class StringPartsConstraint extends Constraint {
+
+  /**
+   * The error message if the string does not match.
+   *
+   * @var string
+   */
+  public string $message = "Expected '@expected_string', not '@value'. Format: '@expected_format'.";
+
+  /**
+   * The separator separating the parts.
+   *
+   * @var string
+   */
+  public string $separator;
+
+  /**
+   * The parent mapping's elements string values that should be used as parts.
+   *
+   * @var array
+   */
+  public array $parts;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions(): array {
+    return ['separator', 'parts'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..c27c0711cfa8361d769bf4853e7f1c47a00c5ff0
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Schema\TypeResolver;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the StringParts constraint.
+ */
+class StringPartsConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    if (!is_string($value)) {
+      throw new UnexpectedTypeException($value, 'string');
+    }
+
+    $resolved_parts = array_map(
+      fn (string $expression): mixed => TypeResolver::resolveExpression($expression, $this->context->getObject()),
+      $constraint->parts
+    );
+
+    // Verify the required parts are present; if not, that's a logical error in
+    // the config schema, not in concrete config.
+    $missing_properties = array_intersect($constraint->parts, $resolved_parts);
+    if (!empty($missing_properties)) {
+      throw new \LogicException(sprintf('This validation constraint is configured to inspect the properties %s, but some do not exist: %s.',
+        implode(', ', $constraint->parts),
+        implode(', ', $missing_properties)
+      ));
+    }
+
+    // Retrieve the parts of the expected string.
+    $expected_string_parts = [];
+    foreach ($constraint->parts as $index => $part) {
+      $part_value = $resolved_parts[$index];
+      if (!is_string($part_value)) {
+        throw new \LogicException(sprintf('The "%s" property does not contain a string, but a %s: "%s".', $part, gettype($part_value), (string) $part_value));
+      }
+      $expected_string_parts[] = $part_value;
+    }
+    $expected_string = implode($constraint->separator, $expected_string_parts);
+
+    if ($expected_string !== $value) {
+      $expected_format = implode(
+        $constraint->separator,
+        array_map(function (string $v) {
+          return sprintf('<%s>', $v);
+        }, $constraint->parts)
+      );
+      $this->context->addViolation($constraint->message, [
+        '@value' => $value,
+        '@expected_string' => $expected_string,
+        '@expected_format' => $expected_format,
+      ]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0b9dab8604df97beaa0a9f664549451ea93beac
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+
+/**
+ * Helper methods for tree-aware validation constraints.
+ *
+ * @todo Figure out whether to deal with TraversableTypedDataInterface, ComplexDataInterface, Mapping, or something else 😬
+ * @todo Similarly, figure out whether to return TypedDataInterface or \Drupal\Core\Config\Schema\Element 😬
+ */
+trait TreeAwareConstraintTrait {
+
+  /**
+   * Finds the parent property.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The parent property.
+   */
+  private function getParentProperty(): TypedDataInterface {
+    $parent_property_path = array_slice(explode('.', $this->context->getPropertyPath()), 0, -1);
+    return self::findPropertyForPath($this->context->getRoot(), $parent_property_path);
+  }
+
+  /**
+   * Finds the specified property path in the given tree.
+   *
+   * @todo consider adopting Symfony's PropertyAccess component.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $tree
+   *   A config schema (sub)tree.
+   * @param string[] $property_path
+   *   A property path, in array form.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The property found at the specified property path.
+   *
+   * @throws \OutOfRangeException
+   *   When requesting a non-existent property path.
+   */
+  private static function findPropertyForPath(ComplexDataInterface $tree, array $property_path): TypedDataInterface {
+    // Edge case: root is requested.
+    if (empty($property_path)) {
+      return $tree;
+    }
+
+    $elements = $tree->getElements();
+    $name = array_shift($property_path);
+
+    if (!isset($elements[$name])) {
+      throw new \OutOfRangeException();
+    }
+    if (empty($property_path)) {
+      return $elements[$name];
+    }
+    return self::findPropertyForPath($elements[$name], $property_path);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a4464a319ce2383ddc5c364ddf0be30820a111d
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a set of config entity fields has a unique combination.
+ *
+ * @Constraint(
+ *   id = "UniqueFieldCombination",
+ *   label = @Translation("Unique field combination constraint", context = "Validation"),
+ * )
+ */
+class UniqueFieldCombinationConstraint extends Constraint {
+
+  /**
+   * @var string $message The error message.
+   */
+  public $message = 'A @entity_type with this combination of values for the fields @field_name_list (%field_value_list) already exists.';
+
+  /**
+   * Fields which must have a unique combination among all entities of a type.
+   *
+   * @var string[]
+   */
+  public array $fields;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption(): string {
+    return 'fields';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions(): array {
+    return ['fields'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b3d6162a2cb58f11a4c4e4b470dfb890404079b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates that a field is unique for the given config entity type.
+ */
+class UniqueFieldCombinationConstraintValidator extends ConstraintValidator {
+
+  use TreeAwareConstraintTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    assert($constraint instanceof UniqueFieldCombinationConstraint);
+
+    if (!is_array($value)) {
+      throw new UnexpectedTypeException($value, 'array');
+    }
+
+    // Find the parent mapping.
+    $mapping = $this->getParentProperty();
+
+    // Verify the required fields are present; if not, that's a logical error in
+    // the config schema, not in concrete config.
+    $properties = $mapping->getProperties();
+    $missing_fields = array_diff($constraint->fields, array_keys($properties));
+    if (!empty($missing_fields)) {
+      throw new \LogicException(sprintf('This validation constraint is configured to inspect the fields %s, but some do not exist: %s.',
+        implode(', ', $constraint->fields),
+        implode(', ', $missing_fields)
+      ));
+    }
+
+    // Construct an entity query.
+    // @see \Drupal\Core\Config\ConfigImporter::checkOp()
+    // @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
+    $config_manager = \Drupal::service('config.manager');
+    $entity_type_id = $config_manager->getEntityTypeIdByName($mapping->getRoot()->getName());
+    $entity_storage = $config_manager->getEntityTypeManager()->getStorage($entity_type_id);
+    $query = $entity_storage->getQuery()
+      ->accessCheck(FALSE);
+    foreach ($constraint->fields as $field_name) {
+      $query->condition($field_name, $mapping->get($field_name)->getValue());
+    }
+
+    $uuid = $mapping->get('uuid')->getValue();
+    if (!empty($uuid)) {
+      $query->condition('uuid', $uuid, '<>');
+    }
+
+    $value_taken = (bool) $query
+      ->range(0, 1)
+      ->execute();
+
+    if ($value_taken) {
+      $this->context->addViolation($constraint->message, [
+        '@entity_type' => $entity_type_id,
+        '@field_name_list' => implode(', ', $constraint->fields),
+        '%field_value_list' => implode(', ', array_map(fn (string $field_name) => $mapping->get($field_name)->getValue(), $constraint->fields)),
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index cdd1d4bab05cbeba55c7f2abc1aa80a567ded688..b0a1dd0f84d40f5d650036e5726c5fcbbbb1e6c3 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -17,20 +17,40 @@ field.settings:
 field.storage.*.*:
   type: config_entity
   label: 'Field'
+  constraints:
+    FullyValidatable: ~
+    UniqueFieldCombination: [entity_type, field_name]
   mapping:
     id:
       type: string
       label: 'ID'
+      constraints:
+        StringParts:
+          separator: .
+          parts:
+            - '%parent.entity_type'
+            - '%parent.field_name'
     field_name:
       type: string
       label: 'Field name'
+
+      constraints:
+        NotBlank: {}
+        Length:
+          # @see \Drupal\field\Entity\FieldStorageConfig::NAME_MAX_LENGTH
+          max: 32
     entity_type:
       type: string
       label: 'Entity type'
+      constraints:
+        PluginExists:
+          manager: entity_type.manager
+          interface: '\Drupal\Core\Entity\FieldableEntityInterface'
     type:
       type: string
       label: 'Type'
       constraints:
+        # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists:
           manager: plugin.manager.field.field_type
           interface: '\Drupal\Core\Field\FieldItemInterface'
@@ -39,18 +59,24 @@ field.storage.*.*:
     module:
       type: string
       label: 'Module'
+      constraints:
+        ExtensionExists: 'module'
     locked:
       type: boolean
       label: 'Locked'
     cardinality:
       type: integer
       label: 'Maximum number of values users can enter'
+      constraints: {}
+      # @todo Create this constraint.
+      # NoEntitiesExistYetWithHigherCardinality: []
     translatable:
       type: boolean
       label: 'Translatable'
     indexes:
       type: sequence
       label: 'Indexes'
+      # @todo Constraints — but shockingly this A) uses `type: ignore`, B) has zero validation logic in core?!
       sequence:
         type: sequence
         label: 'Indexes'
@@ -67,3 +93,17 @@ field.storage.*.*:
 field.field.*.*.*:
   type: field_config_base
   label: 'Field'
+  mapping:
+    # Extend `field_config_base:field_type` with a FieldConfig entity-specific
+    # constraint. (This does not apply to BaseFieldOverride entities.)
+    field_type:
+      constraints:
+        MatchesOtherConfigValue:
+          # This field_storage_config entity must exist. This is already guaranteed.
+          # @see \Drupal\field\Entity\FieldConfig
+          # @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraint
+          prefix: field.storage.
+          id: ['%parent.entity_type', '%parent.field_name']
+          propertyPath: type
+  constraints:
+    FullyValidatable: ~
diff --git a/core/modules/field/field.post_update.php b/core/modules/field/field.post_update.php
index 3af41a483b1c715c33f20eaf86736cd4507a0f9b..f575fa16c476ed966fad2e31c7d8d9b976015556 100644
--- a/core/modules/field/field.post_update.php
+++ b/core/modules/field/field.post_update.php
@@ -5,6 +5,38 @@
  * Post update functions for Field module.
  */
 
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Drupal\field\FieldConfigInterface;
+
+/**
+ * Converts empty `description` on fields to NULL.
+ */
+function field_post_update_set_field_config_empty_description_to_null(array &$sandbox): void {
+  \Drupal::classResolver(ConfigEntityUpdater::class)
+    ->update($sandbox, 'field_config', function (FieldConfigInterface $entity): bool {
+      if (trim($entity->getDescription()) === '') {
+        $entity->set('description', NULL);
+        return TRUE;
+      }
+      return FALSE;
+    });
+}
+
+/**
+ * Converts empty `description` on base field overrides to NULL.
+ */
+function field_post_update_set_base_field_override_empty_description_to_null(array &$sandbox): void {
+  \Drupal::classResolver(ConfigEntityUpdater::class)
+    ->update($sandbox, 'base_field_override', function (BaseFieldOverride $entity): bool {
+      if (trim($entity->getDescription()) === '') {
+        $entity->set('description', NULL);
+        return TRUE;
+      }
+      return FALSE;
+    });
+}
+
 /**
  * Implements hook_removed_post_updates().
  */
diff --git a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
index 0809ab417261e0defd697c0fe3f1a523c46b3fc4..dd73d7679815c94b2b52f2e300847dc858bb686d 100644
--- a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
+++ b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
@@ -79,7 +79,7 @@ protected function getExpectedNormalizedEntity() {
           'text',
         ],
       ],
-      'description' => '',
+      'description' => NULL,
       'entity_type' => 'node',
       'field_name' => 'field_llama',
       'field_type' => 'text',
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 288c1babca0054a74f3f9f73195550aeb441018a..84ecf06874d5c81010a529eff49b3f3dace24bad 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -4,25 +4,26 @@
 
 namespace Drupal\Tests\field\Kernel\Entity;
 
+use Drupal\entity_test\Entity\EntityTestMulBundle;
+use Drupal\field\FieldStorageConfigInterface;
 use Drupal\entity_test\Entity\EntityTestBundle;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
-use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 
 /**
  * Tests validation of field_config entities.
  *
  * @group field
  */
-class FieldConfigValidationTest extends ConfigEntityValidationTestBase {
-
-  use ContentTypeCreationTrait;
+class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
 
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['field', 'node', 'entity_test', 'text', 'user'];
+  protected static array $propertiesWithOptionalValues = [
+    'default_value',
+    'description',
+  ];
 
   /**
    * {@inheritdoc}
@@ -32,13 +33,33 @@ protected function setUp(): void {
 
     $this->installEntitySchema('node');
     $this->installConfig('node');
-    $this->createContentType(['type' => 'one']);
-    $this->createContentType(['type' => 'another']);
 
     EntityTestBundle::create(['id' => 'one'])->save();
+    EntityTestMulBundle::create(['id' => 'one'])->save();
+
+    // Specifically create a bundle for `entity_test_bundle` and
+    // `entity_test_mul_with_bundle` content entities confusingly named
+    // `another`, to allow testing the modifying of the `entity_type` field on
+    // FieldConfig entities without triggering additional validation errors.
+    // @see ::providerImmutableFields()
     EntityTestBundle::create(['id' => 'another'])->save();
+    EntityTestMulBundle::create([
+      'id' => 'another',
+      'label' => $this->randomString(),
+    ])->save();
 
-    $this->entity = FieldConfig::loadByName('node', 'one', 'body');
+    // The field storage was created in the parent method.
+    $field_storage = $this->entity;
+
+    $this->entity = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'one',
+      'settings' => [
+        'on_label' => 'Hello!',
+        'off_label' => 'Goodbye.',
+      ],
+    ]);
+    $this->entity->save();
   }
 
   /**
@@ -67,13 +88,93 @@ public function testInvalidDependencies(): void {
     ]);
   }
 
+  /**
+   * Tests that the field type plugin is validated.
+   */
+  public function testFieldTypePlugin(): void {
+    $this->entity->set('field_type', 'non_existent');
+    // If we don't clear the previous settings here, we will get unrelated
+    // validation errors (in addition to the one we're expecting), because the
+    // settings from the *old* field_type won't match the config schema for the
+    // settings of the *new* field_type.
+    $this->entity->set('settings', []);
+    $this->assertValidationErrors([
+      '' => "The 'field_type' property cannot be changed.",
+      'field_type' => [
+        "The 'non_existent' plugin does not exist.",
+        "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'non_existent'.",
+      ],
+    ]);
+  }
+
+  /**
+   * Tests that the target entity type is validated.
+   *
+   * 90% identical to the parent: an additional validation error is triggered
+   * due to bundle validation.
+   */
+  public function testEntityType(): void {
+    // Ensure the target entity type is valid to begin with.
+    $this->assertValidationErrors([]);
+
+    // Ensure that it is at least plausible that the `entity_type` is modified:
+    // a corresponding FieldStorageConfig must exist due to the entity-level
+    // `RequiredConfigDependencies` constraint.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'test',
+      'entity_type' => 'entity_test',
+    ])->save();
+    $this->entity->set('entity_type', 'entity_test');
+    $this->assertValidationErrors([
+      '' => "The 'entity_type' property cannot be changed.",
+      'bundle' => "The 'one' bundle does not exist on the 'entity_test' entity type.",
+    ]);
+  }
+
+  /**
+   * Tests that the bundle is validated.
+   */
+  public function testBundle(): void {
+    $entity_type_id = 'entity_test_mul_with_bundle';
+    $field_name = 'test';
+
+    // Assert that the FieldStorageConfig which this FieldConfig will depend on,
+    // already exists.
+    $field_storage_config = FieldStorageConfig::loadByName($entity_type_id, $field_name);
+    $this->assertInstanceOf(FieldStorageConfigInterface::class, $field_storage_config);
+
+    // Try to create an instance of this field on a bundle that does not exist.
+    $this->entity = FieldConfig::create([
+      'bundle' => 'non_existent',
+      'field_storage' => $field_storage_config,
+    ]);
+    // The field storage is not listed in the entity's config dependencies,
+    // because the dependencies have not been recalculated yet. They can't be
+    // recalculated until the target bundle exists, or we'll get an exception.
+    // So, for testing purposes, use reflection to access the protected
+    // addDependency() method and add the field storage as a dependency.
+    // @see \Drupal\Core\Field\FieldConfigBase::calculateDependencies()
+    (new \ReflectionMethod($this->entity, 'addDependency'))
+      ->invoke($this->entity, 'config', $field_storage_config->getConfigDependencyName());
+    $this->assertValidationErrors([
+      'bundle' => "The 'non_existent' bundle does not exist on the 'entity_test_mul_with_bundle' entity type.",
+    ]);
+
+    // Next, try to create it on a bundle that *does* exist. We need to
+    // recalculate dependencies because that's how we check whether or not the
+    // bundle exists.
+    $this->entity->set('bundle', 'one')->calculateDependencies();
+    $this->assertValidationErrors([]);
+  }
+
   /**
    * Tests validation of a field_config's default value.
    */
   public function testMultilineTextFieldDefaultValue(): void {
     $this->installEntitySchema('user');
     // First, create a field storage for which a complex default value exists.
-    $this->enableModules(['text']);
+    $this->enableModules(['text', 'user']);
     $text_field_storage_config = FieldStorageConfig::create([
       'type' => 'text_with_summary',
       'field_name' => 'novel',
@@ -107,23 +208,41 @@ public function testTargetBundleMustExist(): void {
     $this->entity->set('bundle', 'nope');
     $this->assertValidationErrors([
       '' => "The 'bundle' property cannot be changed.",
-      'bundle' => "The 'nope' bundle does not exist on the 'node' entity type.",
+      'bundle' => "The 'nope' bundle does not exist on the 'entity_test_mul_with_bundle' entity type.",
     ]);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
-    // If we don't clear the previous settings here, we will get unrelated
-    // validation errors (in addition to the one we're expecting), because the
-    // settings from the *old* field_type won't match the config schema for the
-    // settings of the *new* field_type.
-    $this->entity->set('settings', []);
-    parent::testImmutableProperties([
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
+    // Ensure that it is at least plausible that the `entity_type` is modified:
+    // a corresponding FieldStorageConfig must exist due to the entity-level
+    // `RequiredConfigDependencies` constraint.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'test',
       'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    // Same thing for `field_name`.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'foobar',
+      'entity_type' => 'entity_test_mul_with_bundle',
+    ])->save();
+
+    parent::testImmutableProperties([
       'bundle' => 'another',
-      'field_type' => 'string',
+      'field_type' => 'email',
+      'field_name' => 'foobar',
+    ], [
+      'field_type' => [
+        'field_type' => "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'email'.",
+        'settings' => [
+          "'on_label' is an unknown key because field_type is email (see config schema type field.field_settings.email).",
+          "'off_label' is an unknown key because field_type is email (see config schema type field.field_settings.email).",
+        ],
+      ],
     ]);
   }
 
@@ -137,6 +256,13 @@ public function testRequiredPropertyKeysMissing(?array $additional_expected_vali
         // @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator
         '' => 'This field requires a field storage.',
       ],
+      // If the settings are removed, we should see errors about them missing.
+      'settings' => [
+        'settings' => [
+          "'on_label' is a required key because field_type is boolean (see config schema type field.field_settings.boolean).",
+          "'off_label' is a required key because field_type is boolean (see config schema type field.field_settings.boolean).",
+        ],
+      ],
     ]);
   }
 
@@ -166,7 +292,10 @@ public function testFieldTypePluginIsValidated(): void {
       ->set('field_type', 'invalid');
 
     $this->assertValidationErrors([
-      'field_type' => "The 'invalid' plugin does not exist.",
+      'field_type' => [
+        "The 'invalid' plugin does not exist.",
+        "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'invalid'.",
+      ],
     ]);
   }
 
@@ -185,6 +314,7 @@ public function testEntityReferenceSelectionHandlerIsValidated(): void {
       ->set('settings', ['handler' => 'non_existent']);
 
     $this->assertValidationErrors([
+      'field_type' => "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'entity_reference'.",
       'settings.handler' => "The 'non_existent' plugin 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
index 47532f40c84b9543e5082443b6be7e1f00a9c52d..b23857064e087dde50b49f32cb5c4fef530231b9 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -17,19 +17,19 @@ class FieldStorageConfigValidationTest extends ConfigEntityValidationTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['field', 'node', 'user'];
+  protected static $modules = ['field', 'entity_test'];
 
   /**
    * {@inheritdoc}
    */
   protected function setUp(): void {
     parent::setUp();
-    $this->installEntitySchema('user');
 
     $this->entity = FieldStorageConfig::create([
       'type' => 'boolean',
       'field_name' => 'test',
-      'entity_type' => 'user',
+      'entity_type' => 'entity_test_mul_with_bundle',
+      'custom_storage' => FALSE,
     ]);
     $this->entity->save();
   }
@@ -37,9 +37,43 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
-    $valid_values['type'] = 'string';
-    parent::testImmutableProperties($valid_values);
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
+    parent::testImmutableProperties($valid_values + [
+      'entity_type' => 'entity_test_with_bundle',
+      'type' => 'email',
+    ], $additional_expected_validation_errors_when_modified);
+  }
+
+  /**
+   * Tests that the field type plugin is validated.
+   */
+  public function testFieldTypePlugin(): void {
+    $this->entity->set('type', 'non_existent');
+    $this->assertValidationErrors([
+      '' => "The 'type' property cannot be changed.",
+      'type' => "The 'non_existent' plugin does not exist.",
+    ]);
+  }
+
+  /**
+   * Tests that the target entity type is validated.
+   */
+  public function testEntityType(): void {
+    // Ensure the target entity type is valid to begin with.
+    $this->assertValidationErrors([]);
+
+    $this->entity->set('entity_type', 'strange_entity');
+    $this->assertValidationErrors([
+      '' => "The 'entity_type' property cannot be changed.",
+      'entity_type' => "The 'strange_entity' plugin does not exist.",
+    ]);
+
+    // A valid, but non-fieldable, entity type should raise an error.
+    $this->entity->set('entity_type', 'field_config');
+    $this->assertValidationErrors([
+      '' => "The 'entity_type' property cannot be changed.",
+      'entity_type' => "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
+    ]);
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/FieldCrudTest.php b/core/modules/field/tests/src/Kernel/FieldCrudTest.php
index e51a2db9d6e803af31c06869fd84687e88a3e090..bb56dba0fa43ce933d902727a4b842bfd5f6bce4 100644
--- a/core/modules/field/tests/src/Kernel/FieldCrudTest.php
+++ b/core/modules/field/tests/src/Kernel/FieldCrudTest.php
@@ -95,7 +95,7 @@ public function testCreateField(): void {
     // Check that default values are set.
     $this->assertFalse($config['required'], 'Required defaults to false.');
     $this->assertSame($config['label'], $this->fieldDefinition['field_name'], 'Label defaults to field name.');
-    $this->assertSame('', $config['description'], 'Description defaults to empty string.');
+    $this->assertNull($config['description'], 'Description defaults to NULL.');
 
     // Check that default settings are set.
     $this->assertEquals($config['settings'], $field_type_manager->getDefaultFieldSettings($this->fieldStorageDefinition['type']), 'Default field settings have been written.');
diff --git a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
index d2ee111c28c107c95728f8417620eca35b4d5bd8..77c0e45806be45495caa808879ace9e689849a11 100644
--- a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
@@ -103,7 +103,7 @@ protected function getExpectedDocument(): array {
               'node.type.camelids',
             ],
           ],
-          'description' => '',
+          'description' => NULL,
           'entity_type' => 'node',
           'field_name' => 'promote',
           'field_type' => 'boolean',
diff --git a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
index 6d48f81ed965feb9fbea194c6b3774441e8fc140..345a03d2d48d68a51bc0587e7799b9991670715a 100644
--- a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
@@ -115,7 +115,7 @@ protected function getExpectedDocument(): array {
               'text',
             ],
           ],
-          'description' => '',
+          'description' => NULL,
           'entity_type' => 'node',
           'field_name' => 'field_llama',
           'field_type' => 'text',
diff --git a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
index 056779a0c2ff8cea4d15d90d38e4ad433cb93c74..319975b9e753a1bc8319d47d0ac17026880a7a1b 100644
--- a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
+++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
@@ -70,7 +70,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'target_entity_type_id' => 'entity_test_with_bundle',
       'target_bundle' => 'bravo',
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
index 7f632d87d7d7167195656b8fc80b41321e4cc63c..df689aff532229e69b060b745273bf74e707eee7 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
@@ -71,7 +71,7 @@ public function testLabelValidation(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'id' => 'entity_test_with_bundle.two.full',
       'targetEntityType' => 'entity_test_with_bundle',
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
index 24c071a501ca64f5fae26b2206c9eadace0f3f6b..021f956bf8a21a53aad5ab89ec8c9546f410b765 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
@@ -32,6 +32,9 @@ public function testAudioTypeCreation(): void {
     $type_name = 'audio_type';
     $field_name = 'field_media_' . $source_id;
     $this->doTestCreateMediaType($type_name, $source_id);
+    // @todo This line can be removed when the entity field manager's cache
+    //   is automatically reset when a field is created.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
 
     // Check that the source field was created with the correct settings.
     $storage = FieldStorageConfig::load("media.$field_name");
@@ -81,6 +84,9 @@ public function testVideoTypeCreation(): void {
     $type_name = 'video_type';
     $field_name = 'field_media_' . $source_id;
     $this->doTestCreateMediaType($type_name, $source_id);
+    // @todo This line can be removed when the entity field manager's cache
+    //   is automatically reset when a field is created.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
 
     // Check that the source field was created with the correct settings.
     $storage = FieldStorageConfig::load("media.$field_name");
diff --git a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
index 4fbc4e424bdcfd833fb8cdc15ed6d094fb992b0d..89e124204fe9e390b0b878c82cfb5fa4a9d1b5aa 100644
--- a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
+++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
@@ -36,7 +36,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     // If we don't clear the previous settings here, we will get unrelated
     // validation errors (in addition to the one we're expecting), because the
     // settings from the *old* source won't match the config schema for the
diff --git a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
index 1e3efa75252bf0a1e36c66fae212004a09950107..3e259eaec9e7ce9feb0574b419e590e81a0b3646 100644
--- a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
+++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
@@ -62,7 +62,7 @@ public function testInvalidPluginId(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'test_changed';
     parent::testImmutableProperties($valid_values);
   }
diff --git a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
index 7003ecded8b09a99540f58d21feba9b5b1d9622e..942165ddf70794933fcd2234acfca61d25c5a84a 100644
--- a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
@@ -66,7 +66,7 @@ protected function getExpectedNormalizedEntity() {
           'node.type.camelids',
         ],
       ],
-      'description' => '',
+      'description' => NULL,
       'entity_type' => 'node',
       'field_name' => 'promote',
       'field_type' => 'boolean',
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
index 18cd991e38d8a30d35783c1bcfe51dab56152249..c1143b1bc1e13233b08a973a441699daf7e1e762 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -469,15 +469,20 @@ public function testLangcode(): void {
    *   (optional) The values to set for the immutable properties, keyed by name.
    *   This should be used if the immutable properties can only accept certain
    *   values, e.g. valid plugin IDs.
+   * @param string[]|null $additional_expected_validation_errors_when_modified
+   *   Some immutable config entity properties have additional validation
+   *   constraints that cause additional messages to appear. Keys must be
+   *   config entity properties, values must be arrays as expected by
+   *   ::assertValidationErrors().
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $constraints = $this->entity->getEntityType()->getConstraints();
     $this->assertNotEmpty($constraints['ImmutableProperties'], 'All config entities should have at least one immutable ID property.');
 
     foreach ($constraints['ImmutableProperties'] as $property_name) {
       $original_value = $this->entity->get($property_name);
       $this->entity->set($property_name, $valid_values[$property_name] ?? $this->randomMachineName());
-      $this->assertValidationErrors([
+      $this->assertValidationErrors(($additional_expected_validation_errors_when_modified[$property_name] ?? []) + [
         '' => "The '$property_name' property cannot be changed.",
       ]);
       $this->entity->set($property_name, $original_value);
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index 6f71621dbb308dcacd70d8b60e8129e6929270c3..cbdb61c77560ae42fe459a106acc4e4ce1471274 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -6,6 +6,8 @@
 
 use Drupal\Core\Field\Entity\BaseFieldOverride;
 use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 
@@ -19,6 +21,14 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
 
   use ContentTypeCreationTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected static array $propertiesWithOptionalValues = [
+    'default_value',
+    'description',
+  ];
+
   /**
    * {@inheritdoc}
    */
@@ -45,6 +55,25 @@ protected function setUp(): void {
     $this->entity->save();
   }
 
+  /**
+   * Tests that the bundle is validated.
+   */
+  public function testBundle(): void {
+    $fields = $this->container->get('entity_field.manager')
+      ->getBaseFieldDefinitions('user');
+
+    // Try to create an instance of this base field override on a bundle that
+    // does not exist.
+    $this->entity = BaseFieldOverride::createFromBaseFieldDefinition($fields['uuid'], 'non_existent');
+    $this->assertValidationErrors([
+      'bundle' => "The 'non_existent' bundle does not exist on the 'user' entity type.",
+    ]);
+
+    // Next, try to create it on a bundle that does exist.
+    $this->entity = BaseFieldOverride::createFromBaseFieldDefinition($fields['uuid'], 'user');
+    $this->assertValidationErrors([]);
+  }
+
   /**
    * Tests that the target bundle of the field is checked.
    */
@@ -59,7 +88,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     // If we don't clear the previous settings here, we will get unrelated
     // validation errors (in addition to the one we're expecting), because the
     // settings from the *old* field_type won't match the config schema for the
@@ -68,7 +97,8 @@ public function testImmutableProperties(array $valid_values = []): void {
     parent::testImmutableProperties([
       'entity_type' => 'entity_test_with_bundle',
       'bundle' => 'another',
-      'field_type' => 'string',
+      'field_type' => 'email',
+      'field_name' => 'title',
     ]);
   }
 
@@ -85,4 +115,30 @@ public function testFieldTypePluginIsValidated(): void {
     ]);
   }
 
+  /**
+   * Tests that base field overrides must be overriding, well, base fields.
+   */
+  public function testOverriddenFieldMustBeABaseField(): void {
+    $storage = FieldStorageConfig::create([
+      'entity_type' => 'node',
+      'field_name' => 'field_mail',
+      'type' => 'email',
+    ]);
+    $storage->save();
+
+    $field = FieldConfig::create([
+      'field_storage' => $storage,
+      'bundle' => 'one',
+    ]);
+    $field->save();
+
+    // The `field_name` property is immutable, so we need to clone the entity
+    // in order to change it.
+    $this->entity = $this->entity->createDuplicate()
+      ->set('field_name', 'field_mail');
+    $this->assertValidationErrors([
+      '' => "'field_mail' is not a base field of the node entity type.",
+    ]);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
index fa9754dfe9b6d183a6e28e2b3ea4a7cde3bea07d..85aa6b651e1034de447a62c3a612b0e4a8056c73 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
@@ -113,7 +113,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'id' => 'entity_test_with_bundle.two.default',
       'targetEntityType' => 'entity_test_with_bundle',
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
index c9db7a0ef442ea564f908c8b11c041c0bcba2c7f..eea7df90b6dbb3cc7bf6cc06efead921ea0e8b3f 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
@@ -39,7 +39,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'user.test_changed';
     parent::testImmutableProperties($valid_values);
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
index 1b5797f0101db6dce9c1d0910e44c007a06fb90e..22a5bdc716b5c09d210d4f9d0a7980f5c504a0d3 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
@@ -91,7 +91,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'id' => 'entity_test_with_bundle.two.full',
       'targetEntityType' => 'entity_test_with_bundle',
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
index 1f5e3bca4b34d0c3250eaa13ec15bc4be7c05248..61ad67001435933589470ef06a65311ddcfcc15f 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
@@ -39,7 +39,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'user.test_changed';
     parent::testImmutableProperties($valid_values);
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
index 6471e2e60e8038f39856de9192c62d1d34c26e9a..b364a89867a7386b506c3ff0ad3081d5d4e832ae 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
@@ -47,8 +47,13 @@ public function testValidation(): void {
     $this->enableModules(['user']);
     $this->assertCount(0, $data->validate());
 
-    // NULL should not trigger a validation error: a value may be nullable.
-    $data->setValue(NULL);
+    // Special case: the `core` module — this is not a real module but is the
+    // official module-like extension that provides many plugins.
+    $data = $typed_data->create($definition, 'core');
+    $this->assertCount(0, $data->validate());
+    // Special case: `NULL` — validation constraints should be compatible with
+    // optional values.
+    $data = $typed_data->create($definition, NULL);
     $this->assertCount(0, $data->validate());
 
     $definition->setConstraints(['ExtensionExists' => 'theme']);