 ...onditionalCacheabilityMetadataBubblingTrait.php |  32 ++++
 core/modules/filter/src/Element/ProcessedText.php  |   7 +-
 core/modules/filter/src/FilterProcessResult.php    |   2 +-
 .../tests/src/Kernel/EntitySerializationTest.php   |  34 +++-
 .../text/src/Normalizer/TextItemBaseNormalizer.php | 101 ++++++++++++
 .../Normalizer/TextItemBaseNormalizerTest.php      | 183 +++++++++++++++++++++
 core/modules/text/text.services.yml                |   8 +
 7 files changed, 361 insertions(+), 6 deletions(-)

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 4b07fad..02c89d0 100644
--- a/core/modules/filter/src/FilterProcessResult.php
+++ b/core/modules/filter/src/FilterProcessResult.php
@@ -77,7 +77,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/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 0f84d3d..ad23a04 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..a1701a6
--- /dev/null
+++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\text\Normalizer;
+
+use Drupal\Core\Cache\ConditionalCacheabilityMetadataBubblingTrait;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\filter\FilterProcessResult;
+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 = TextItemBase::class;
+
+  /**
+   * 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);
+    $processed_text = $this->computeProcessedAttribute($field_item);
+    $this->bubble($processed_text);
+    $attributes['processed'] = $this->serializer->normalize($processed_text->getProcessedText(), $format, $context);
+    return $attributes;
+  }
+
+  /**
+   * Computes the 'processed' property of TextItemBase fields.
+   *
+   * The 'processed' property for fields that extend TextItemBase will
+   * not contain cacheability metadata
+   * (see \Drupal\text\TextProcessed::getValue()). Therefore this function
+   * returns a FilterProcessResult which can carry both the processed text and
+   * the cacheability metadata. The cacheability metadata can be bubbled up in
+   * GET responses that use this normalization so that the responses can be
+   * cached (and invalidated) correctly.
+   *
+   * @param \Drupal\text\Plugin\Field\FieldType\TextItemBase $field_item
+   *   The field item.
+   *
+   * @return \Drupal\filter\FilterProcessResult
+   *   The filter process result for the text item.
+   *
+   * @see \Drupal\text\TextProcessed::getValue()
+   * @see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions()
+   */
+  protected function computeProcessedAttribute(TextItemBase $field_item) {
+    $value = $field_item->getValue();
+    $build = [
+      '#type' => 'processed_text',
+      '#text' => $value['value'],
+      '#format' => $value['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->renderer->renderPlain($build);
+    return FilterProcessResult::createFromRenderArray($build)->setProcessedText($processed_text);
+  }
+
+}
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 }
+
