diff --git a/wagtail/blocks/base.py b/wagtail/blocks/base.py
index 5c4ce2f2c3..cd73c8098e 100644
--- a/wagtail/blocks/base.py
+++ b/wagtail/blocks/base.py
@@ -756,10 +756,22 @@ class BlockField(forms.Field):
         super().__init__(**kwargs)
 
     def clean(self, value):
-        # Pass required flag to the top-level block, so that dynamically setting it on the
-        # field (e.g. by defer_required_fields) is respected by the block.
-        self.block.set_meta_options({"required": self.required})
-        return self.block.clean(value)
+        from wagtail.blocks.stream_block import StreamBlock
+
+        if isinstance(self.block, StreamBlock):
+            # StreamBlock is the only block type that is formally-supported as the top level block
+            # of a BlockField, but it's possible that other block types could be used, so check
+            # this explicitly.
+            # self.block has a `required` attribute that is consistent with the StreamField's `blank`
+            # attribute and thus the `required` attribute of BlockField - but if the latter has been
+            # assigned dynamically (e.g. by defer_required_fields) we want this to take precedence.
+            # We do this through the `ignore_required_constraints` flag recognised by
+            # StreamBlock.clean.
+            return self.block.clean(
+                value, ignore_required_constraints=not self.required
+            )
+        else:
+            return self.block.clean(value)
 
     def has_changed(self, initial_value, data_value):
         return self.block.get_prep_value(initial_value) != self.block.get_prep_value(
diff --git a/wagtail/blocks/stream_block.py b/wagtail/blocks/stream_block.py
index 95991ff28a..343754b76d 100644
--- a/wagtail/blocks/stream_block.py
+++ b/wagtail/blocks/stream_block.py
@@ -160,7 +160,7 @@ class BaseStreamBlock(Block):
     def required(self):
         return self.meta.required
 
-    def clean(self, value):
+    def clean(self, value, ignore_required_constraints=False):
         cleaned_data = []
         errors = {}
         non_block_errors = ErrorList()
@@ -179,7 +179,7 @@ class BaseStreamBlock(Block):
                     % {"min_num": self.meta.min_num}
                 )
             )
-        elif self.required and len(value) == 0:
+        elif self.required and not ignore_required_constraints and len(value) == 0:
             non_block_errors.append(ValidationError(_("This field is required.")))
 
         if self.meta.max_num is not None and self.meta.max_num < len(value):