diff --git a/core/composer.json b/core/composer.json
index 71f07d0..2e32005 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -9,6 +9,7 @@
         "symfony/console": "~2.8",
         "symfony/dependency-injection": "~2.8",
         "symfony/dom-crawler": ">=2.8.13 <3.0",
+
         "symfony/event-dispatcher": "~2.8",
         "symfony/http-foundation": "~2.8",
         "symfony/http-kernel": "~2.8",
diff --git a/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
new file mode 100644
index 0000000..b8c4b33
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Provides bubble function to apply cacheable dependency to render context.
+ *
+ * This trait should be used with great care. It should only be used by classes
+ * that may be used both inside and outside of a render context.
+ * For example:
+ * - Generating URLs for CLI vs for HTTP responses.
+ * - Serializing/normalizing data for scripts vs for HTTP responses.
+ */
+trait ConditionalCacheabilityMetadataBubblingTrait {
+
+  /**
+   * Bubbles cacheability metadata to the current render context.
+   *
+   * This method does not bubble attachments.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $object
+   *   A cacheable dependency object.
+   */
+  protected function bubble(CacheableDependencyInterface $object) {
+    if ($this->renderer->hasRenderContext()) {
+      $build = [];
+      CacheableMetadata::createFromObject($object)->applyTo($build);
+      $this->renderer->render($build);
+    }
+  }
+
+}
diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php
index 0d09870..27b797e 100644
--- a/core/modules/filter/src/Element/ProcessedText.php
+++ b/core/modules/filter/src/Element/ProcessedText.php
@@ -3,6 +3,7 @@
 namespace Drupal\filter\Element;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\filter\Entity\FilterFormat;
@@ -69,7 +70,11 @@ public static function preRenderText($element) {
     $langcode = $element['#langcode'];
 
     if (!isset($format_id)) {
-      $format_id = static::configFactory()->get('filter.settings')->get('fallback_format');
+      $filter_settings = static::configFactory()->get('filter.settings');
+      $format_id = $filter_settings->get('fallback_format');
+      CacheableMetadata::createFromRenderArray($element)
+        ->addCacheableDependency($filter_settings)
+        ->applyTo($element);
     }
     /** @var \Drupal\filter\Entity\FilterFormat $format **/
     $format = FilterFormat::load($format_id);
diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php
index 3fa592b..2a8a71b 100644
--- a/core/modules/filter/src/FilterProcessResult.php
+++ b/core/modules/filter/src/FilterProcessResult.php
@@ -78,7 +78,7 @@ class FilterProcessResult extends BubbleableMetadata {
    * @param string $processed_text
    *   The text as processed by a text filter.
    */
-  public function __construct($processed_text) {
+  public function __construct($processed_text = '') {
     $this->processedText = $processed_text;
   }
 
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index 376a444..d778f21 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -21,7 +21,13 @@ class FieldItemNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function normalize($field_item, $format = NULL, array $context = array()) {
-    $values = $field_item->toArray();
+    $values = [];
+    // We normalize each individual property, so each can do their own casting,
+    // if needed.
+    /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+    foreach ($field_item as $property_name => $property) {
+      $values[$property_name] = $this->serializer->normalize($property, $format, $context);
+    }
     if (isset($context['langcode'])) {
       $values['lang'] = $context['langcode'];
     }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
index 2e332bd..5b7908e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -5,6 +5,7 @@
 use Drupal\comment\Entity\Comment;
 use Drupal\comment\Entity\CommentType;
 use Drupal\comment\Tests\CommentTestTrait;
+use Drupal\Core\Cache\Cache;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
 use Drupal\user\Entity\User;
@@ -200,6 +201,7 @@ protected function getExpectedNormalizedEntity() {
         [
           'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
           'format' => 'plain_text',
+          'processed' => '<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>' . "\n",
         ],
       ],
     ];
@@ -252,6 +254,20 @@ protected function getNormalizedPatchEntity() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheTags() {
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheContexts() {
+    return $this->container->getParameter('renderer.config')['required_cache_contexts'];
+  }
+
+  /**
    * Tests POSTing a comment without critical base fields.
    *
    * testPost() is testing with the most minimal normalization possible: the one
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index b6dce4f..e0f26cf 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\rest\Functional\EntityResource\Term;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\Entity\Vocabulary;
 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
@@ -64,6 +65,7 @@ protected function createEntity() {
     // Create a "Llama" taxonomy term.
     $term = Term::create(['vid' => $vocabulary->id()])
       ->setName('Llama')
+      ->setDescription("It is a little known fact that llamas cannot count higher the seven.")
       ->setChangedTime(123456789);
     $term->save();
 
@@ -93,8 +95,9 @@ protected function getExpectedNormalizedEntity() {
       ],
       'description' => [
         [
-          'value' => NULL,
+          'value' => 'It is a little known fact that llamas cannot count higher the seven.',
           'format' => NULL,
+          'processed' => "<p>It is a little known fact that llamas cannot count higher the seven.</p>\n",
         ],
       ],
       'parent' => [],
@@ -137,4 +140,18 @@ protected function getNormalizedPostEntity() {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheTags() {
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheContexts() {
+    return $this->container->getParameter('renderer.config')['required_cache_contexts'];
+  }
+
 }
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 8d7fdab..255e5b6 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\filter\Entity\FilterFormat;
 
 /**
  * Tests that entities can be serialized to supported core formats.
@@ -60,6 +61,29 @@ protected function setUp() {
     // User create needs sequence table.
     $this->installSchema('system', array('sequences'));
 
+    // Create a text format because it is needed for TextItemBase normalization.
+    // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize().
+    FilterFormat::create([
+      'format' => 'my_text_format',
+      'name' => 'My Text Format',
+      'filters' => [
+        'filter_html' => [
+          'module' => 'filter',
+          'status' => TRUE,
+          'weight' => 10,
+          'settings' => [
+            'allowed_html' => '<p>',
+          ],
+        ],
+        'filter_autop' => [
+          'module' => 'filter',
+          'status' => TRUE,
+          'weight' => 10,
+          'settings' => [],
+        ],
+      ],
+    ])->save();
+
     // Create a test user to use as the entity owner.
     $this->user = \Drupal::entityManager()->getStorage('user')->create([
       'name' => 'serialization_test_user',
@@ -69,12 +93,13 @@ protected function setUp() {
     $this->user->save();
 
     // Create a test entity to serialize.
+    $test_text_value = $this->randomMachineName();
     $this->values = array(
       'name' => $this->randomMachineName(),
       'user_id' => $this->user->id(),
       'field_test_text' => array(
-        'value' => $this->randomMachineName(),
-        'format' => 'full_html',
+        'value' => $test_text_value,
+        'format' => 'my_text_format',
       ),
     );
     $this->entity = EntityTestMulRev::create($this->values);
@@ -127,6 +152,7 @@ public function testNormalize() {
         array(
           'value' => $this->values['field_test_text']['value'],
           'format' => $this->values['field_test_text']['format'],
+          'processed' => "<p>{$this->values['field_test_text']['value']}</p>"
         ),
       ),
     );
@@ -134,7 +160,7 @@ public function testNormalize() {
     $normalized = $this->serializer->normalize($this->entity);
 
     foreach (array_keys($expected) as $fieldName) {
-      $this->assertEqual($expected[$fieldName], $normalized[$fieldName], "ComplexDataNormalizer produces expected array for $fieldName.");
+      $this->assertEquals($expected[$fieldName], $normalized[$fieldName], "Field normalization produces expected array for $fieldName.");
     }
     $this->assertEqual(array_diff_key($normalized, $expected), array(), 'No unexpected data is added to the normalized array.');
   }
@@ -192,7 +218,7 @@ public function testSerialize() {
       'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
       'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
       'non_rev_field' => '<non_rev_field/>',
-      'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
+      'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format><processed><![CDATA[<p>' . $this->values['field_test_text']['value'] . '</p>]]></processed></field_test_text>',
     );
     // Sort it in the same order as normalised.
     $expected = array_merge($normalized, $expected);
diff --git a/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
new file mode 100644
index 0000000..ebaf5b3
--- /dev/null
+++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\text\Normalizer;
+
+use Drupal\Core\Cache\ConditionalCacheabilityMetadataBubblingTrait;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\serialization\Normalizer\ComplexDataNormalizer;
+use Drupal\text\Plugin\Field\FieldType\TextItemBase;
+
+/**
+ * Adds processed text from text fields to normalizer data.
+ *
+ * This class does not implement DenormalizerInterface because the 'processed'
+ * attribute is computed and represents text processed by the filter system
+ * therefore it cannot be provided when creating new entities.
+ */
+class TextItemBaseNormalizer extends ComplexDataNormalizer {
+
+  use ConditionalCacheabilityMetadataBubblingTrait;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = 'Drupal\text\Plugin\Field\FieldType\TextItemBase';
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a TextItemBaseNormalizer object.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   */
+  public function __construct(RendererInterface $renderer, ConfigFactoryInterface $config_factory) {
+    $this->renderer = $renderer;
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field_item, $format = NULL, array $context = []) {
+    $attributes = parent::normalize($field_item, $format, $context);
+    /** @var \Drupal\filter\FilterProcessResult $processed_text */
+    $processed_text = $field_item->process_result;
+    $this->bubble($processed_text);
+    $attributes['processed'] = $this->serializer->normalize($processed_text->getProcessedText(), $format, $context);
+    return $attributes;
+  }
+
+}
diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
index 83cea4f..c6b73e4 100644
--- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
+++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
@@ -7,6 +7,8 @@
 use Drupal\Core\Field\FieldItemBase;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\TypedData\DataDefinition;
+use Drupal\text\TextProcessed;
+use Drupal\text\TextProcessedResult;
 
 /**
  * Base class for 'text' configurable field types.
@@ -31,6 +33,13 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
       ->setClass('\Drupal\text\TextProcessed')
       ->setSetting('text source', 'value');
 
+    $properties['process_result'] = DataDefinition::create('string')
+      ->setLabel(t('Processed text (object)'))
+      ->setDescription(t('The text with the text format applied.'))
+      ->setComputed(TRUE)
+      ->setClass(TextProcessedResult::class)
+      ->setSetting('text source', 'value');
+
     return $properties;
   }
 
@@ -57,7 +66,7 @@ public function isEmpty() {
   public function onChange($property_name, $notify = TRUE) {
     // Unset processed properties that are affected by the change.
     foreach ($this->definition->getPropertyDefinitions() as $property => $definition) {
-      if ($definition->getClass() == '\Drupal\text\TextProcessed') {
+      if (in_array($definition->getClass(), [TextProcessed::class, TextProcessedResult::class], TRUE)) {
         if ($property_name == 'format' || ($definition->getSetting('text source') == $property_name)) {
           $this->writePropertyValue($property, NULL);
         }
diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php
index 61bef37..3038397 100644
--- a/core/modules/text/src/TextProcessed.php
+++ b/core/modules/text/src/TextProcessed.php
@@ -2,9 +2,7 @@
 
 namespace Drupal\text;
 
-use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\Core\TypedData\TypedDataInterface;
-use Drupal\Core\TypedData\TypedData;
+use Drupal\Core\Render\Markup;
 
 /**
  * A computed property for processing text with a format.
@@ -12,56 +10,19 @@
  * Required settings (below the definition's 'settings' key) are:
  *  - text source: The text property containing the to be processed text.
  */
-class TextProcessed extends TypedData {
-
-  /**
-   * Cached processed text.
-   *
-   * @var string|null
-   */
-  protected $processed = NULL;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(DataDefinitionInterface $definition, $name = NULL, TypedDataInterface $parent = NULL) {
-    parent::__construct($definition, $name, $parent);
-
-    if ($definition->getSetting('text source') === NULL) {
-      throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed.");
-    }
-  }
+class TextProcessed extends TextProcessedResult {
 
   /**
    * {@inheritdoc}
    */
   public function getValue() {
-    if ($this->processed !== NULL) {
-      return $this->processed;
-    }
-
-    $item = $this->getParent();
-    $text = $item->{($this->definition->getSetting('text source'))};
+    $value = parent::getValue();
 
-    // Avoid running check_markup() on empty strings.
-    if (!isset($text) || $text === '') {
-      $this->processed = '';
-    }
-    else {
-      $this->processed = check_markup($text, $item->format, $item->getLangcode());
-    }
-    return $this->processed;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setValue($value, $notify = TRUE) {
-    $this->processed = $value;
-    // Notify the parent of any changes.
-    if ($notify && isset($this->parent)) {
-      $this->parent->onChange($this->name);
+    if ($value !== '') {
+      /** @var \Drupal\filter\FilterProcessResult $value */
+      $value = Markup::create((string) $value);
     }
+    return $value;
   }
 
 }
diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessedResult.php
similarity index 64%
copy from core/modules/text/src/TextProcessed.php
copy to core/modules/text/src/TextProcessedResult.php
index 61bef37..6857036 100644
--- a/core/modules/text/src/TextProcessed.php
+++ b/core/modules/text/src/TextProcessedResult.php
@@ -5,6 +5,7 @@
 use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\TypedData\TypedData;
+use Drupal\filter\FilterProcessResult;
 
 /**
  * A computed property for processing text with a format.
@@ -12,12 +13,12 @@
  * Required settings (below the definition's 'settings' key) are:
  *  - text source: The text property containing the to be processed text.
  */
-class TextProcessed extends TypedData {
+class TextProcessedResult extends TypedData {
 
   /**
    * Cached processed text.
    *
-   * @var string|null
+   * @var \Drupal\filter\FilterProcessResult|string|null
    */
   protected $processed = NULL;
 
@@ -48,7 +49,17 @@ public function getValue() {
       $this->processed = '';
     }
     else {
-      $this->processed = check_markup($text, $item->format, $item->getLangcode());
+      $build = [
+        '#type' => 'processed_text',
+        '#text' => $text,
+        '#format' => $item->format,
+        '#filter_types_to_skip' => [],
+        '#langcode' => '',
+      ];
+      // It's necessary to capture the cacheability metadata associated with the
+      // processed text. See https://www.drupal.org/node/2278483.
+      $processed_text = $this->getRenderer()->renderPlain($build);
+      $this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text);
     }
     return $this->processed;
   }
@@ -64,4 +75,13 @@ public function setValue($value, $notify = TRUE) {
     }
   }
 
+  /**
+   * Returns the renderer service.
+   *
+   * @return \Drupal\Core\Render\RendererInterface
+   */
+  protected function getRenderer() {
+    return \Drupal::service('renderer');
+  }
+
 }
diff --git a/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
new file mode 100644
index 0000000..0d217a2
--- /dev/null
+++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\Tests\text\Kernel\Normalizer;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\text\Normalizer\TextItemBaseNormalizer
+ * @group text
+ */
+class TextItemBaseNormalizerTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test', 'serialization', 'text', 'field', 'user', 'filter', 'filter_test'];
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected $serializer;
+
+  /**
+   * The text format ID of the fallback format.
+   *
+   * In a non-unit test, this would be read from the 'filter.settings' config,
+   * from the 'fallback_format' key.
+   *
+   * @var string
+   */
+  protected static $fallbackFormatId = 'plain_text';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->serializer = \Drupal::service('serializer');
+
+    $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('user');
+    $this->installConfig('filter');
+
+    FieldStorageConfig::create([
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_text',
+      'type' => 'text',
+    ])->save();
+
+    FieldConfig::create([
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_text',
+      'bundle' => 'entity_test',
+    ])->save();
+
+    FilterFormat::create([
+      'format' => 'my_text_format',
+      'name' => 'My text format',
+      'filters' => [
+        'filter_autop' => [
+          'module' => 'filter',
+          'status' => TRUE,
+        ],
+        'filter_html' => [
+          'status' => TRUE,
+          'settings' => [
+            'allowed_html' => '<strong>',
+          ],
+        ],
+        // Include this test filter because it bubbles cache tags.
+        'filter_test_cache_tags' => [
+          'status' => TRUE,
+        ],
+        // Include this test filter because it bubbles cache contexts.
+        'filter_test_cache_contexts' => [
+          'status' => TRUE,
+        ],
+      ],
+    ])->save();
+  }
+
+  /**
+   * @covers ::normalize
+   *
+   * @dataProvider testNormalizeProvider
+   */
+  public function testNormalize($text_item, array $expected, CacheableMetadata $extra_cacheability, array $filter_config_update = [], $updated_processed = '') {
+    $original_entity = EntityTest::create(['field_text' => $text_item]);
+    $original_entity->save();
+    $text_format = FilterFormat::load(empty($text_item['format']) ? static::$fallbackFormatId : $text_item['format']);
+
+    $entity = clone $original_entity;
+    $context = new RenderContext();
+    $data = $this->container->get('renderer')
+      ->executeInRenderContext($context, function () use ($entity) {
+        return $this->serializer->normalize($entity);
+      });
+
+    $expected_cacheability = new BubbleableMetadata();
+    $expected_cacheability->setCacheTags($text_format->getCacheTags());
+    $contexts = $this->container->getParameter('renderer.config')['required_cache_contexts'];
+    $expected_cacheability->setCacheContexts($contexts);
+    // Merge the CacheableMetadata that is specific to this test.
+    $expected_cacheability = $expected_cacheability->merge($extra_cacheability);
+
+    $this->assertEquals($expected_cacheability, $context->pop());
+    $this->assertEquals($expected, $data['field_text'][0]);
+
+    if ($filter_config_update) {
+      // Update format to see if normalization changes.
+      foreach ($filter_config_update as $instance_id => $update) {
+        $text_format->setFilterConfig($instance_id, $update);
+      }
+      $text_format->save();
+
+      $entity = clone $original_entity;
+      $context = new RenderContext();
+      $data = $this->container->get('renderer')
+        ->executeInRenderContext($context, function () use ($entity) {
+          return $this->serializer->normalize($entity);
+        });
+
+      $this->assertFalse($context->isEmpty());
+      $this->assertEquals($expected_cacheability, $context->pop());
+      $expected['processed'] = $updated_processed;
+      $this->assertEquals($expected, $data['field_text'][0]);
+    }
+  }
+
+  /**
+   * Data provider for testNormalize().
+   */
+  public function testNormalizeProvider() {
+    $test_cases['no format specified'] = [
+      'text item',
+      [
+        'value' => 'text item',
+        'processed' => "<p>text item</p>\n",
+        'format' => NULL,
+      ],
+      (new CacheableMetadata())
+        ->setCacheTags(['config:filter.settings']),
+    ];
+    $test_cases['my text format'] = [
+      [
+        'value' => '<strong>This</strong> is <b>important.</b>',
+        'format' => 'my_text_format',
+      ],
+      [
+        'value' => "<strong>This</strong> is <b>important.</b>",
+        'format' => 'my_text_format',
+        'processed' => "<p><strong>This</strong> is important.</p>\n",
+      ],
+      (new CacheableMetadata())
+        // Add the tags for the 'filter_test_cache_tags' filter.
+        ->setCacheTags(['foo:bar', 'foo:baz'])
+        // Add the contexts for the 'filter_test_cache_contexts' filter.
+        ->setCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]),
+      [
+        'filter_html' => [
+          'status' => TRUE,
+          'settings' => [
+            'allowed_html' => '<strong> <b>',
+          ],
+        ],
+      ],
+      "<p><strong>This</strong> is <b>important.</b></p>\n",
+    ];
+    return $test_cases;
+  }
+
+}
diff --git a/core/modules/text/text.services.yml b/core/modules/text/text.services.yml
new file mode 100644
index 0000000..0d13007
--- /dev/null
+++ b/core/modules/text/text.services.yml
@@ -0,0 +1,8 @@
+services:
+  serializer.normalizer.text_item_base:
+    class: Drupal\text\Normalizer\TextItemBaseNormalizer
+    arguments: ['@renderer', '@config.factory']
+    tags:
+      # This normalizer needs to be the same or greater priority than serializer.normalizer.field_item.hal
+      - { name: normalizer, priority: 10 }
+
