diff --git a/src/ClientDataToEntityConverter.php b/src/ClientDataToEntityConverter.php
index cf2e99650d2c8356b93adf937cff4a50f84bd80c..18e16bb10785099226d3e9723824e09debe9a601 100644
--- a/src/ClientDataToEntityConverter.php
+++ b/src/ClientDataToEntityConverter.php
@@ -170,7 +170,14 @@ class ClientDataToEntityConverter {
       }
     }
 
-    $entity_form_fields = \array_filter($entity_form_fields, static fn (array|string $value, string|int $key): bool => !\in_array($key, $boolean_fields, TRUE) || $value !== ['value' => '0'], ARRAY_FILTER_USE_BOTH);
+    $entity_form_fields = \array_combine(\array_keys($entity_form_fields), \array_map(static fn (string|int $key): array|string =>
+      // Unchecked boolean checkboxes are expected to be set with value NULL. For a normal
+      // form submission, this is done for us by the Form Builder. But for a
+      // programmatic form submission, this needs to be done manually.
+      // @see \Drupal\Core\Form\FormBuilder::handleInputElement
+      (!\in_array($key, $boolean_fields, TRUE) || $entity_form_fields[$key] !== ['value' => '0']) ? $entity_form_fields[$key] : ['value' => NULL],
+      \array_keys($entity_form_fields),
+    ));
     $form_object = $this->entityTypeManager->getFormObject($entity->getEntityTypeId(), 'default');
     $form_object->setEntity($entity);
     // Flag this as a programmatic build of the entity form - but do not flag
diff --git a/tests/modules/xb_test_article_fields/xb_test_article_fields.install b/tests/modules/xb_test_article_fields/xb_test_article_fields.install
index 2e3d315966739d8b77bc35faad4d5a8044dd516d..019662e723c95832344c54972160626c94c7b796 100644
--- a/tests/modules/xb_test_article_fields/xb_test_article_fields.install
+++ b/tests/modules/xb_test_article_fields/xb_test_article_fields.install
@@ -328,6 +328,36 @@ function xb_test_article_fields_install(): void {
         ],
       ],
     ],
+    'field_xbt_boolean_checkbox' => [
+      'type' => 'boolean',
+      'label' => 'XB Boolean Checkbox (default true)',
+      'settings' => [
+        'on_label' => 'Yes',
+        'off_label' => 'No',
+      ],
+      'widget' => [
+        'type' => 'boolean_checkbox',
+        'settings' => [],
+      ],
+      'default_value' => [
+        ['value' => TRUE],
+      ],
+    ],
+    'field_xbt_boolean_checkbox2' => [
+      'type' => 'boolean',
+      'label' => 'XB Boolean Checkbox (default false)',
+      'settings' => [
+        'on_label' => 'Yes',
+        'off_label' => 'No',
+      ],
+      'widget' => [
+        'type' => 'boolean_checkbox',
+        'settings' => [],
+      ],
+      'default_value' => [
+        ['value' => FALSE],
+      ],
+    ],
   ];
 
   // Setup content moderation for article node type.
diff --git a/ui/tests/e2e/entity-form-fields/field_xbt_boolean_checkbox.js b/ui/tests/e2e/entity-form-fields/field_xbt_boolean_checkbox.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ac2ecd09ed93098b2e05bf4540645f5043d425e
--- /dev/null
+++ b/ui/tests/e2e/entity-form-fields/field_xbt_boolean_checkbox.js
@@ -0,0 +1,25 @@
+export const edit = (cy) => {
+  cy.findByLabelText('XB Boolean Checkbox (default true)').as('checkbox');
+  cy.get('@checkbox').should('have.attr', 'aria-checked', 'true');
+  cy.get('@checkbox').click();
+  cy.get('@checkbox').should('have.attr', 'aria-checked', 'false');
+  // Wait for the preview to finish loading.
+  cy.wait('@updatePreview');
+  cy.findByLabelText('Loading Preview').should('not.exist');
+
+  // Trigger a new intercept for the main test to wait for.
+  cy.intercept({
+    url: '**/xb/api/layout/node/2',
+    times: 1,
+    method: 'POST',
+  }).as('updatePreview');
+  cy.findByLabelText('XB Boolean Checkbox (default false)').as('checkbox');
+  cy.get('@checkbox').should('have.attr', 'aria-checked', 'false');
+  cy.get('@checkbox').click();
+  cy.get('@checkbox').should('have.attr', 'aria-checked', 'true');
+};
+
+export const assertData = (response) => {
+  expect(response.attributes.field_xbt_boolean_checkbox).to.eq(false);
+  expect(response.attributes.field_xbt_boolean_checkbox2).to.eq(true);
+};
diff --git a/ui/tests/e2e/entity-form-fields/index.js b/ui/tests/e2e/entity-form-fields/index.js
index 7af9a0726311e37c8eef8b69adba49b0382b9939..69179123b3ce134df02cbd2ce542fe3960a7743b 100644
--- a/ui/tests/e2e/entity-form-fields/index.js
+++ b/ui/tests/e2e/entity-form-fields/index.js
@@ -13,6 +13,7 @@ import * as field_xbt_datetime_timestamp from './field_xbt_datetime_timestamp.js
 import * as field_xbt_daterange_datelist from './field_xbt_daterange_datelist.js';
 import * as field_xbt_datetime_datelist from './field_xbt_datetime_datelist.js';
 import * as field_xbt_entity_ref_tags from './field_xbt_entity_ref_tags.js';
+import * as field_xbt_boolean_checkbox from './field_xbt_boolean_checkbox.js';
 
 // Expand this to add additional coverage.
 // For each field to be tested, add a new file that exports two methods as
@@ -41,4 +42,5 @@ export default {
   field_xbt_daterange_datelist,
   field_xbt_datetime_datelist,
   field_xbt_entity_ref_tags,
+  field_xbt_boolean_checkbox,
 };