diff --git a/core/lib/Drupal/Component/Utility/SafeStringInterface.php b/core/lib/Drupal/Component/Utility/SafeStringInterface.php
index 136dd37..45d536d 100644
--- a/core/lib/Drupal/Component/Utility/SafeStringInterface.php
+++ b/core/lib/Drupal/Component/Utility/SafeStringInterface.php
@@ -33,7 +33,7 @@
  * @see \Drupal\Component\Utility\SafeMarkup::isSafe()
  * @see \Drupal\Core\Template\TwigExtension::escapeFilter()
  */
-interface SafeStringInterface {
+interface SafeStringInterface extends \JsonSerializable {
 
   /**
    * Returns a safe string.
diff --git a/core/lib/Drupal/Component/Utility/SafeStringTrait.php b/core/lib/Drupal/Component/Utility/SafeStringTrait.php
index a91e44b..77afc8a 100644
--- a/core/lib/Drupal/Component/Utility/SafeStringTrait.php
+++ b/core/lib/Drupal/Component/Utility/SafeStringTrait.php
@@ -67,4 +67,14 @@ public function count() {
     return Unicode::strlen($this->string);
   }
 
+  /**
+   * Returns a representation of the object for use in JSON serialisation.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index bffe666..7d36b51 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -164,15 +164,15 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req
     $resource_commands = array();
     if ($css_assets) {
       $css_render_array = $this->cssCollectionRenderer->render($css_assets);
-      $resource_commands[] = new AddCssCommand((string) $this->renderer->renderPlain($css_render_array));
+      $resource_commands[] = new AddCssCommand($this->renderer->renderPlain($css_render_array));
     }
     if ($js_assets_header) {
       $js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
-      $resource_commands[] = new PrependCommand('head', (string) $this->renderer->renderPlain($js_header_render_array));
+      $resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
     }
     if ($js_assets_footer) {
       $js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
-      $resource_commands[] = new AppendCommand('body', (string) $this->renderer->renderPlain($js_footer_render_array));
+      $resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
     }
     foreach (array_reverse($resource_commands) as $resource_command) {
       $response->addCommand($resource_command, TRUE);
diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
index d83db57..e48d060 100644
--- a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
+++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php
@@ -29,7 +29,7 @@
    * If content is a render array, it may contain attached assets to be
    * processed.
    *
-   * @return string
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
    *   HTML rendered content.
    */
   protected function getRenderedContent() {
@@ -37,10 +37,10 @@ protected function getRenderedContent() {
     if (is_array($this->content)) {
       $html = \Drupal::service('renderer')->renderRoot($this->content);
       $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
-      return (string) $html;
+      return $html;
     }
     else {
-      return (string) $this->content;
+      return $this->content;
     }
   }
 
diff --git a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
index c54799d..31192f3 100644
--- a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
@@ -64,7 +64,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
       }
     }
 
-    $html = (string) $this->drupalRenderRoot($main_content);
+    $html = $this->drupalRenderRoot($main_content);
     $response->setAttachments($main_content['#attached']);
 
     // The selector for the insert command is NULL as the new content will
@@ -72,7 +72,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     // behavior can be changed with #ajax['method'].
     $response->addCommand(new InsertCommand(NULL, $html));
     $status_messages = array('#type' => 'status_messages');
-    $output = (string) $this->drupalRenderRoot($status_messages);
+    $output = $this->drupalRenderRoot($status_messages);
     if (!empty($output)) {
       $response->addCommand(new PrependCommand(NULL, $output));
     }
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
index 6bf591a..e764ed5 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -118,4 +118,14 @@ public function __sleep() {
     return array('string', 'arguments', 'options');
   }
 
+  /**
+   * Returns a representation of the object for use in JSON serialisation.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function jsonSerialize() {
+    return $this->__toString();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index 8fe6788..dc4c13b 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -302,4 +302,14 @@ public function storage() {
     return $this->storage;
   }
 
+  /**
+   * Returns a representation of the object for use in JSON serialisation.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function jsonSerialize() {
+    return (string) $this;
+  }
+
 }
diff --git a/core/modules/contextual/src/ContextualController.php b/core/modules/contextual/src/ContextualController.php
index ca248d1..975113d 100644
--- a/core/modules/contextual/src/ContextualController.php
+++ b/core/modules/contextual/src/ContextualController.php
@@ -44,7 +44,7 @@ public function render(Request $request) {
         '#type' => 'contextual_links',
         '#contextual_links' => _contextual_id_to_links($id),
       );
-      $rendered[$id] = (string) $this->container->get('renderer')->renderRoot($element);
+      $rendered[$id] = $this->container->get('renderer')->renderRoot($element);
     }
 
     return new JsonResponse($rendered);
diff --git a/core/modules/editor/src/EditorController.php b/core/modules/editor/src/EditorController.php
index 192f341..44feda3 100644
--- a/core/modules/editor/src/EditorController.php
+++ b/core/modules/editor/src/EditorController.php
@@ -48,7 +48,7 @@ public function getUntransformedText(EntityInterface $entity, $field_name, $lang
     // Direct text editing is only supported for single-valued fields.
     $field = $entity->getTranslation($langcode)->$field_name;
     $editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
-    $response->addCommand(new GetUntransformedTextCommand((string) $editable_text));
+    $response->addCommand(new GetUntransformedTextCommand($editable_text));
 
     return $response;
   }
diff --git a/core/modules/quickedit/src/QuickEditController.php b/core/modules/quickedit/src/QuickEditController.php
index 067a73a..bf0b62d 100644
--- a/core/modules/quickedit/src/QuickEditController.php
+++ b/core/modules/quickedit/src/QuickEditController.php
@@ -216,7 +216,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
       $response->addCommand(new FieldFormSavedCommand($output, $other_view_modes));
     }
     else {
-      $output = (string) $this->renderer->renderRoot($form);
+      $output = $this->renderer->renderRoot($form);
       // When working with a hidden form, we don't want its CSS/JS to be loaded.
       if ($request->request->get('nocssjs') !== 'true') {
         $response->setAttachments($form['#attached']);
@@ -228,7 +228,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
         $status_messages = array(
           '#type' => 'status_messages'
         );
-        $response->addCommand(new FieldFormValidationErrorsCommand((string) $this->renderer->renderRoot($status_messages)));
+        $response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages)));
       }
     }
 
@@ -255,7 +255,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
    *   The view mode the field should be rerendered in. Either an Entity Display
    *   view mode ID, or a custom one. See hook_quickedit_render_field().
    *
-   * @return string
+   * @return \Drupal\Component\Utility\SafeStringInterface
    *   Rendered HTML.
    *
    * @see hook_quickedit_render_field()
@@ -275,7 +275,7 @@ protected function renderField(EntityInterface $entity, $field_name, $langcode,
       $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
     }
 
-    return (string) $this->renderer->renderRoot($output);
+    return $this->renderer->renderRoot($output);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index 1278a6d..444caba 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\SafeStringTrait;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -234,19 +235,5 @@ public function __toString() {
  * SafeMarkup::set() is a global static that affects all tests.
  */
 class SafeMarkupTestSafeString implements SafeStringInterface {
-
-  protected $string;
-
-  public function __construct($string) {
-    $this->string = $string;
-  }
-
-  public function __toString() {
-    return $this->string;
-  }
-
-  public static function create($string) {
-    $safe_string = new static($string);
-    return $safe_string;
-  }
+  use SafeStringTrait;
 }
