 .../src/Functional/CookieResourceTestTrait.php     |   2 +-
 .../EntityResource/EntityResourceTestBase.php      |  60 ++++++++++-
 .../EntityResource/Node/NodeXmlAnonTest.php        |  26 +++++
 .../EntityResource/Node/NodeXmlBasicAuthTest.php   |  36 +++++++
 .../EntityResource/Node/NodeXmlCookieTest.php      |  31 ++++++
 .../XmlEntityNormalizationQuirksTrait.php          | 111 +++++++++++++++++++++
 .../src/Functional/XmlNormalizationQuirksTrait.php |  33 ++++++
 7 files changed, 294 insertions(+), 5 deletions(-)

diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
index 8975c3f..689fa83 100644
--- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -61,7 +61,7 @@ protected function initAuthentication() {
       'pass' => $this->account->passRaw,
     ];
 
-    $request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, 'json');
+    $request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, static::$format);
     $request_options[RequestOptions::HEADERS] = [
       'Content-Type' => static::$mimeType,
     ];
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 72589bd..419490b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -180,11 +180,29 @@ public function setUp() {
         ->setTranslatable(FALSE)
         ->save();
 
-      // Reload entity so that it has the new field.
+      // Add multi-value field.
+      FieldStorageConfig::create([
+        'entity_type' => static::$entityTypeId,
+        'field_name' => 'field_rest_test_multivalue',
+        'type' => 'string',
+      ])
+        ->setCardinality(2)
+        ->save();
+      FieldConfig::create([
+        'entity_type' => static::$entityTypeId,
+        'field_name' => 'field_rest_test_multivalue',
+        'bundle' => $this->entity->bundle(),
+      ])
+        ->setLabel('Test field: multi-value')
+        ->setTranslatable(FALSE)
+        ->save();
+
+      // Reload entity so that it has the new fields.
       $this->entity = $this->entityStorage->loadUnchanged($this->entity->id());
 
-      // Set a default value on the field.
+      // Set a default value on the fields.
       $this->entity->set('field_rest_test', ['value' => 'All the faith he had had had had no effect on the outcome of his life.']);
+      $this->entity->set('field_rest_test_multivalue', [['value' => 'One'], ['value' => 'Two']]);
       $this->entity->save();
     }
   }
@@ -460,6 +478,16 @@ public function testGet() {
     // for the keys with the array order the same (it needs to match with
     // identical comparison).
     $expected = $this->getExpectedNormalizedEntity();
+    $expected += [
+      'field_rest_test_multivalue' => [
+        0 => [
+          'value' => 'One',
+        ],
+        1 => [
+          'value' => 'Two',
+        ],
+      ]
+    ];
     static::recursiveKSort($expected);
     $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
     static::recursiveKSort($actual);
@@ -467,8 +495,12 @@ public function testGet() {
 
     // Not only assert the normalization, also assert deserialization of the
     // response results in the expected object.
-    $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
-    $this->assertSame($unserialized->uuid(), $this->entity->uuid());
+    // Note: deserialization of the XML format is not supported, so only test
+    // this for other formats.
+    if (static::$format !== 'xml') {
+      $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
+      $this->assertSame($unserialized->uuid(), $this->entity->uuid());
+    }
     // Finally, assert that the expected 'Link' headers are present.
     if ($this->entity->getEntityType()->getLinkTemplates()) {
       $this->assertArrayHasKey('Link', $response->getHeaders());
@@ -522,6 +554,16 @@ public function testGet() {
       // normalized entity's values to strings. This ensures the BC layer for
       // bc_primitives_as_strings works as expected.
       $expected = $this->getExpectedNormalizedEntity();
+      $expected += [
+        'field_rest_test_multivalue' => [
+          0 => [
+            'value' => 'One',
+          ],
+          1 => [
+            'value' => 'Two',
+          ],
+        ]
+      ];
       // Config entities are not affected.
       // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
       $expected = static::castToString($expected);
@@ -557,6 +599,16 @@ public function testGet() {
       // ::formatExpectedTimestampValue() to generate the timestamp value. This
       // will take into account the above config setting.
       $expected = $this->getExpectedNormalizedEntity();
+      $expected += [
+        'field_rest_test_multivalue' => [
+          0 => [
+            'value' => 'One',
+          ],
+          1 => [
+            'value' => 'Two',
+          ],
+        ]
+      ];
       // Config entities are not affected.
       // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
       static::recursiveKSort($expected);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlAnonTest.php
new file mode 100644
index 0000000..eb06390
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlAnonTest.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\Node;
+
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * @group rest
+ */
+class NodeXmlAnonTest extends NodeResourceTestBase {
+
+  use AnonResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlBasicAuthTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlBasicAuthTest.php
new file mode 100644
index 0000000..93838f2
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlBasicAuthTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\Node;
+
+use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * @group rest
+ */
+class NodeXmlBasicAuthTest extends NodeResourceTestBase {
+
+  use BasicAuthResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['basic_auth'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'basic_auth';
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlCookieTest.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlCookieTest.php
new file mode 100644
index 0000000..39ae7dd
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeXmlCookieTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\Node;
+
+use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
+use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
+
+/**
+ * @group rest
+ */
+class NodeXmlCookieTest extends NodeResourceTestBase {
+
+  use CookieResourceTestTrait;
+  use XmlEntityNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'xml';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'text/xml; charset=UTF-8';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $auth = 'cookie';
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php
new file mode 100644
index 0000000..bed4f26
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource;
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\ChangedItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\CreatedItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem;
+use Drupal\path\Plugin\Field\FieldType\PathItem;
+use Drupal\Tests\rest\Functional\XmlNormalizationQuirksTrait;
+
+/**
+ * Trait for EntityResourceTestBase subclasses testing $format='xml'.
+ */
+trait XmlEntityNormalizationQuirksTrait {
+
+  use XmlNormalizationQuirksTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $default_normalization = parent::getExpectedNormalizedEntity();
+
+    $normalization = $this->applyXmlFieldDecodingQuirks($default_normalization);
+    $normalization = $this->applyXmlDecodingQuirks($normalization);
+
+    return $normalization;
+  }
+
+  /**
+   * Applies the XML entity field encoding quirks that remain after decoding.
+   *
+   * The XML encoding:
+   * - loses type data (int and bool become string)
+   *
+   * @param array $normalization
+   *   An entity normalization.
+   *
+   * @return array
+   *   The updated entity normalization.
+   *
+   * @see \Symfony\Component\Serializer\Encoder\XmlEncoder
+   */
+  protected function applyXmlFieldDecodingQuirks(array $normalization) {
+    if (!$this->entity instanceof FieldableEntityInterface) {
+      throw new \LogicException('This trait should only be used for fieldable entity types.');
+    }
+
+    foreach ($this->entity->getFields(TRUE) as $field_name => $field) {
+      // Not every field is accessible.
+      if (!isset($normalization[$field_name])) {
+        continue;
+      }
+
+      for ($i = 0; $i < count($normalization[$field_name]); $i++) {
+        switch ($field->getItemDefinition()->getClass()) {
+          case BooleanItem::class:
+            $value = &$normalization[$field_name][$i]['value'];
+            $value = $value === TRUE ? '1' : '0';
+            break;
+          case IntegerItem::class:
+            $value = &$normalization[$field_name][$i]['value'];
+            $value = (string) $value;
+            break;
+          case PathItem::class:
+            $pid = &$normalization[$field_name][$i]['pid'];
+            $pid = (string) $pid;
+            break;
+          case EntityReferenceItem::class:
+            $target_id = &$normalization[$field_name][$i]['target_id'];
+            $target_id = (string) $target_id;
+            break;
+          case ChangedItem::class:
+          case CreatedItem::class:
+            $value = &$normalization[$field_name][$i]['value'];
+            if (is_numeric($value)) {
+              $value = (string) $value;
+            }
+
+            break;
+        }
+      }
+
+      if (!empty($normalization[$field_name])) {
+        $normalization[$field_name] = $normalization[$field_name][0];
+      }
+    }
+
+    return $normalization;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPost() {
+    // Deserialization of the XML format is not supported.
+    $this->markTestSkipped();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatch() {
+    // Deserialization of the XML format is not supported.
+    $this->markTestSkipped();
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/XmlNormalizationQuirksTrait.php b/core/modules/rest/tests/src/Functional/XmlNormalizationQuirksTrait.php
new file mode 100644
index 0000000..11c77810
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/XmlNormalizationQuirksTrait.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional;
+
+/**
+ * Trait for ResourceTestBase subclasses testing $format='xml'.
+ */
+trait XmlNormalizationQuirksTrait {
+
+  /**
+   * Applies the XML encoding quirks that remain after decoding.
+   *
+   * The XML encoding:
+   * - maps empty arrays to the empty string
+   *
+   * @param array $normalization
+   *   A normalization.
+   *
+   * @return array
+   *   The updated normalization.
+   *
+   * @see \Symfony\Component\Serializer\Encoder\XmlEncoder
+   */
+  protected function applyXmlDecodingQuirks(array $normalization) {
+    foreach ($normalization as $key => $value) {
+      if ($value === []) {
+        $normalization[$key] = '';
+      }
+    }
+    return $normalization;
+  }
+
+}
