diff --git a/core/lib/Drupal/Core/Field/AllowedTagsXssTrait.php b/core/lib/Drupal/Core/Field/AllowedTagsXssTrait.php
index 6125c5c..95b1864 100644
--- a/core/lib/Drupal/Core/Field/AllowedTagsXssTrait.php
+++ b/core/lib/Drupal/Core/Field/AllowedTagsXssTrait.php
@@ -6,12 +6,13 @@
 
 namespace Drupal\Core\Field;
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Xss;
-
 /**
  * Useful methods when dealing with displaying allowed tags.
+ *
+ * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
+ *   \Drupal\Core\Field\FieldFilteredString instead.
+ *
+ * @see \Drupal\Core\Field\FieldFilteredString
  */
 trait AllowedTagsXssTrait {
 
@@ -33,30 +34,21 @@
    *   valid UTF-8.
    */
   public function fieldFilterXss($string) {
-    // All known XSS vectors are filtered out by
-    // \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
-    // allowed intentionally by the trait, and no danger is added in by
-    // \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
-    // is essentially the same markup, designate this string as safe as well.
-    // This method is an internal part of field sanitization, so the resultant,
-    // sanitized string should be printable as is.
-    //
-    // @todo Free this memory in https://www.drupal.org/node/2505963.
-    return SafeMarkup::set(Html::normalize(Xss::filter($string, $this->allowedTags())));
+   return FieldFilteredString::create($string);
   }
 
   /**
    * Returns a list of tags allowed by AllowedTagsXssTrait::fieldFilterXss().
    */
   public function allowedTags() {
-    return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins',  'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img');
+    return FieldFilteredString::allowedTags();
   }
 
   /**
    * Returns a human-readable list of allowed tags for display in help texts.
    */
   public function displayAllowedTags() {
-    return '<' . implode('> <', $this->allowedTags()) . '>';
+    return FieldFilteredString::displayAllowedTags();
   }
 
 }
diff --git a/core/lib/Drupal/Core/Field/FieldFilteredString.php b/core/lib/Drupal/Core/Field/FieldFilteredString.php
new file mode 100644
index 0000000..c3fca4d
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/FieldFilteredString.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Field\FieldFilteredString.
+ */
+
+namespace Drupal\Core\Field;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\SafeStringTrait;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\Xss;
+
+/**
+ * Defines an object that passes safe strings through the Field system.
+ *
+ * This object should only be constructed with a known safe string. If there is
+ * any risk that the string contains user-entered data that has not been
+ * filtered first, it must not be used.
+ *
+ * @internal
+ *   This object is marked as internal because it should only be used in the
+ *   Filter module on strings that have already been been filtered and sanitized
+ *   in \Drupal\filter\Plugin\FilterInterface.
+ *
+ * @see \Drupal\Core\Render\SafeString
+ */
+final class FieldFilteredString implements SafeStringInterface, \Countable {
+  use SafeStringTrait;
+
+  /**
+   * Overrides \Drupal\Component\Utility\SafeStringTrait::create().
+   *
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   A safe string filtered with the allowed tag list and normalized.
+   *
+   * @see \Drupal\Core\Field\FieldFilteredString::allowedTags()
+   * @see \Drupal\Component\Utility\Xss::filter()
+   * @see \Drupal\Component\Utility\Html::normalize()
+   */
+  public static function create($string) {
+    $string = (string) $string;
+    if ($string === '') {
+      return '';
+    }
+    $safe_string = new static();
+    // All known XSS vectors are filtered out by
+    // \Drupal\Component\Utility\Xss::filter(), all tags in the markup are
+    // allowed intentionally by the trait, and no danger is added in by
+    // \Drupal\Component\Utility\HTML::normalize(). Since the normalized value
+    // is essentially the same markup, designate this string as safe as well.
+    // This method is an internal part of field sanitization, so the resultant,
+    // sanitized string should be printable as is.
+    $safe_string->string = Html::normalize(Xss::filter($string, static::allowedTags()));
+    return $safe_string;
+  }
+
+  /**
+   * Returns the allowed tag list.
+   *
+   * @return string[]
+   *   A list of allowed tags.
+   */
+  public static function allowedTags() {
+    return ['a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins',  'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img'];
+  }
+
+  /**
+   * Returns a human-readable list of allowed tags for display in help texts.
+   *
+   * @return string
+   *   A human-readable list of allowed tags for display in help texts.
+   */
+  public static function displayAllowedTags() {
+    return '<' . implode('> <', static::allowedTags()) . '>';
+  }
+
+  /**
+   * @param $option
+   * @return $this|static
+   *   If the case transformation does not affect the string the same object is
+   *   returned. If it does, then a new object is returned, preserving
+   *   immutability.
+   */
+  public function caseTransform($option) {
+    $new_string = $this->string;
+    switch ($option) {
+      case 'upper':
+        $new_string = Unicode::strtoupper($this->string);
+        break;
+      case 'lower':
+        $new_string = Unicode::strtolower($this->string);
+        break;
+      case 'ucfirst':
+        $new_string = Unicode::ucfirst($this->string);
+        break;
+      case 'ucwords':
+        $new_string = Unicode::ucwords($this->string);
+        break;
+    }
+    if ($new_string !== $this->string) {
+      // The string can be considered safe because it must have been filtered
+      // during construction.
+      $new_object = new static();
+      $new_object->string = $new_string;
+      return $new_object;
+    }
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php
index 50f8006..0172bdb 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Field\Plugin\Field\FieldWidget;
 
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\WidgetBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -101,11 +102,11 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
     // Add prefix and suffix.
     if ($field_settings['prefix']) {
       $prefixes = explode('|', $field_settings['prefix']);
-      $element['#field_prefix'] = $this->fieldFilterXss(array_pop($prefixes));
+      $element['#field_prefix'] = FieldFilteredString::create(array_pop($prefixes));
     }
     if ($field_settings['suffix']) {
       $suffixes = explode('|', $field_settings['suffix']);
-      $element['#field_suffix'] = $this->fieldFilterXss(array_pop($suffixes));
+      $element['#field_suffix'] = FieldFilteredString::create(array_pop($suffixes));
     }
 
     return array('value' => $element);
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php
index 8b53b38..84b7d2c 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/OptionsWidgetBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\WidgetBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -190,7 +191,7 @@ protected function supportsGroups() {
    */
   protected function sanitizeLabel(&$label) {
     // Allow a limited set of HTML tags.
-    $label = $this->fieldFilterXss($label);
+    $label = FieldFilteredString::create($label);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php
index 356eece..444525f 100644
--- a/core/lib/Drupal/Core/Field/WidgetBase.php
+++ b/core/lib/Drupal/Core/Field/WidgetBase.php
@@ -86,7 +86,7 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter
       $delta = isset($get_delta) ? $get_delta : 0;
       $element = array(
         '#title' => SafeMarkup::checkPlain($this->fieldDefinition->getLabel()),
-        '#description' => $this->fieldFilterXss(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
+        '#description' => FieldFilteredString::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
       );
       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
 
@@ -165,7 +165,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
     }
 
     $title = SafeMarkup::checkPlain($this->fieldDefinition->getLabel());
-    $description = $this->fieldFilterXss(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
+    $description = FieldFilteredString::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
 
     $elements = array();
 
diff --git a/core/modules/field_ui/src/Form/FieldConfigEditForm.php b/core/modules/field_ui/src/Form/FieldConfigEditForm.php
index 52ace95..153b8c0 100644
--- a/core/modules/field_ui/src/Form/FieldConfigEditForm.php
+++ b/core/modules/field_ui/src/Form/FieldConfigEditForm.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\field\FieldConfigInterface;
@@ -65,7 +66,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Help text'),
       '#default_value' => $this->entity->getDescription(),
       '#rows' => 5,
-      '#description' => $this->t('Instructions to present to the user below this field on the editing form.<br />Allowed HTML tags: @tags', array('@tags' => $this->displayAllowedTags())) . '<br />' . $this->t('This field supports tokens.'),
+      '#description' => $this->t('Instructions to present to the user below this field on the editing form.<br />Allowed HTML tags: @tags', array('@tags' => FieldFilteredString::displayAllowedTags())) . '<br />' . $this->t('This field supports tokens.'),
       '#weight' => -10,
     );
 
diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
index a2d1fef..fb69d9c 100644
--- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
+++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\WidgetBase;
@@ -119,7 +120,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
     }
 
     $title = SafeMarkup::checkPlain($this->fieldDefinition->getLabel());
-    $description = $this->fieldFilterXss($this->fieldDefinition->getDescription());
+    $description = FieldFilteredString::create($this->fieldDefinition->getDescription());
 
     $elements = array();
 
diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php
index f202293..0a69144 100644
--- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php
+++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\image\Plugin\Field\FieldWidget;
 
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
@@ -97,7 +98,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
     if ($cardinality == 1) {
       // If there's only one field, return it as delta 0.
       if (empty($elements[0]['#default_value']['fids'])) {
-        $file_upload_help['#description'] = $this->fieldFilterXss($this->fieldDefinition->getDescription());
+        $file_upload_help['#description'] = FieldFilteredString::create($this->fieldDefinition->getDescription());
         $elements[0]['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help);
       }
     }
diff --git a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
index 37220f5..0b61f44 100644
--- a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
+++ b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
@@ -8,6 +8,7 @@
 namespace Drupal\options\Plugin\Field\FieldFormatter;
 
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Field\FormatterBase;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\OptGroup;
@@ -48,7 +49,8 @@ public function viewElements(FieldItemListInterface $items) {
         // If the stored value is in the current set of allowed values, display
         // the associated label, otherwise just display the raw value.
         $output = isset($options[$value]) ? $options[$value] : $value;
-        $elements[$delta] = array('#markup' => $this->fieldFilterXss($output));
+        // @todo convert to markup using the tag list.
+        $elements[$delta] = array('#markup' => FieldFilteredString::create($output));
       }
     }
 
diff --git a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsKeyFormatter.php b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsKeyFormatter.php
index f6328a8..8c78816 100644
--- a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsKeyFormatter.php
+++ b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsKeyFormatter.php
@@ -35,7 +35,8 @@ public function viewElements(FieldItemListInterface $items) {
     $elements = array();
 
     foreach ($items as $delta => $item) {
-      $elements[$delta] = array('#markup' => $this->fieldFilterXss($item->value));
+      // @todo convert to markup using the tag list.
+      $elements[$delta] = array('#markup' => FieldFilteredString::create($output));
     }
 
     return $elements;
diff --git a/core/modules/options/src/Plugin/views/argument/NumberListField.php b/core/modules/options/src/Plugin/views/argument/NumberListField.php
index d99d86f..4e22f9c 100644
--- a/core/modules/options/src/Plugin/views/argument/NumberListField.php
+++ b/core/modules/options/src/Plugin/views/argument/NumberListField.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\FieldAPIHandlerTrait;
 use Drupal\views\ViewExecutable;
@@ -80,7 +81,7 @@ public function summaryName($data) {
     $value = $data->{$this->name_alias};
     // If the list element has a human readable name show it.
     if (isset($this->allowedValues[$value]) && !empty($this->options['summary']['human'])) {
-      return $this->fieldFilterXss($this->allowedValues[$value]);
+      return FieldFilteredString::create($this->allowedValues[$value]);
     }
     // Else, fallback to the key.
     else {
diff --git a/core/modules/options/src/Plugin/views/argument/StringListField.php b/core/modules/options/src/Plugin/views/argument/StringListField.php
index a36ef7a..7a39b35 100644
--- a/core/modules/options/src/Plugin/views/argument/StringListField.php
+++ b/core/modules/options/src/Plugin/views/argument/StringListField.php
@@ -8,6 +8,7 @@
 namespace Drupal\options\Plugin\views\argument;
 
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\FieldAPIHandlerTrait;
 use Drupal\views\ViewExecutable;
@@ -80,12 +81,14 @@ public function summaryName($data) {
     $value = $data->{$this->name_alias};
     // If the list element has a human readable name show it.
     if (isset($this->allowedValues[$value]) && !empty($this->options['summary']['human'])) {
-      return $this->caseTransform($this->fieldFilterXss($this->allowedValues[$value]), $this->options['case']);
+      $value = FieldFilteredString::create($this->allowedValues[$value]);
     }
     // Else, fallback to the key.
     else {
-      return $this->caseTransform(SafeMarkup::checkPlain($value), $this->options['case']);
+      // Filtering an escaped string will not change it.
+      $value = FieldFilteredString::create(SafeMarkup::checkPlain($value));
     }
+    return $value->caseTransform($this->options['case']);
   }
 
 }
diff --git a/core/modules/views/src/Plugin/views/argument/FieldList.php b/core/modules/views/src/Plugin/views/argument/FieldList.php
index 9764f0f..36c50ac 100644
--- a/core/modules/views/src/Plugin/views/argument/FieldList.php
+++ b/core/modules/views/src/Plugin/views/argument/FieldList.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -70,7 +71,7 @@ public function summaryName($data) {
     $value = $data->{$this->name_alias};
     // If the list element has a human readable name show it,
     if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) {
-      return $this->fieldFilterXss($this->allowed_values[$value]);
+      return FieldFilteredString::create($this->allowed_values[$value]);
     }
     // else fallback to the key.
     else {
diff --git a/core/modules/views/src/Plugin/views/argument/ListString.php b/core/modules/views/src/Plugin/views/argument/ListString.php
index 202d070..83419eb 100644
--- a/core/modules/views/src/Plugin/views/argument/ListString.php
+++ b/core/modules/views/src/Plugin/views/argument/ListString.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Field\AllowedTagsXssTrait;
+use Drupal\Core\Field\FieldFilteredString;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -72,12 +73,14 @@ public function summaryName($data) {
     $value = $data->{$this->name_alias};
     // If the list element has a human readable name show it,
     if (isset($this->allowed_values[$value]) && !empty($this->options['summary']['human'])) {
-      return $this->caseTransform($this->fieldfilterXss($this->allowed_values[$value]), $this->options['case']);
+      $value = FieldFilteredString::create($this->allowed_values[$value]);
     }
     // else fallback to the key.
     else {
-      return $this->caseTransform(SafeMarkup::checkPlain($value), $this->options['case']);
+      // Filtering an escaped string will not change it.
+      $value = FieldFilteredString::create(SafeMarkup::checkPlain($value));
     }
+    return $value->caseTransform($this->options['case']);
   }
 
 }
