diff --git a/json_ld_schema.module b/json_ld_schema.module
index 8d3a23d9b3672c0d0d7e02a20c9add7e1159a49e..d23f7c0ad47e88d33bc9628a3a3c3c0ec18460ab 100644
--- a/json_ld_schema.module
+++ b/json_ld_schema.module
@@ -63,7 +63,7 @@ function json_ld_schema_entity_view(array &$build, EntityInterface $entity, Enti
         '#tag' => 'script',
         '#attributes' => ['type' => 'application/ld+json'],
         '#value' => json_encode($json_ld_entity->getData($entity, $view_mode)
-          ->toArray(), JSON_UNESCAPED_UNICODE),
+          ->toArray(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT),
       ],
       sprintf('json_ld_%s_%s_%s_%s', $entity->getEntityTypeId(), $entity->id(), $view_mode, $definition['id']),
     ];
diff --git a/src/Element/JsonLdSource.php b/src/Element/JsonLdSource.php
index c93762e655f7852c2cb02ad901d8eda10bae0a06..c50d4fff64fc62992d5a1c0822a4364df735e510 100644
--- a/src/Element/JsonLdSource.php
+++ b/src/Element/JsonLdSource.php
@@ -35,7 +35,7 @@ class JsonLdSource extends RenderElement {
     // that it is cached by render cache.
     $source_manager = \Drupal::service('plugin.manager.json_ld_schema.source');
     $plugin = $source_manager->createInstance($element['#plugin_id']);
-    $element['#value'] = json_encode($plugin->getData()->toArray(), JSON_UNESCAPED_UNICODE);
+    $element['#value'] = json_encode($plugin->getData()->toArray(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
     return $element;
   }
 
diff --git a/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php b/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php
index 9abc8f06150066b1bc4b76e4f08bc4f6e6020ead..5b3b918b4e432e45d24cb61c19c3831025a08c13 100644
--- a/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php
+++ b/tests/module/json_ld_schema_test_sources/src/EntityJsonLdSourceBase.php
@@ -11,11 +11,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Base class for JSON LD sources that should appear on entity pages.
- *
- * @deprecated
- *   Use the JsonLdEntity plugin type instead of this base class. This used to
- *   be a thing until JsonLdEntity, but now it only exists as a good test case
- *   for the render cache integration of JsonLdSource.
  */
 abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements ContainerFactoryPluginInterface {
 
@@ -49,7 +44,7 @@ abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements Contai
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('current_route_match')
+      $container->get('current_route_match'),
     );
   }
 
@@ -60,7 +55,7 @@ abstract class EntityJsonLdSourceBase extends JsonLdSourceBase implements Contai
    *   An entity for FALSE if none was found.
    */
   private function getRawEntityFromRoute() {
-    list($route_type, $entity_type_id, $entity_route_type) = array_pad(explode('.', $this->currentRouteMatch->getRouteName()), 3, NULL);
+    [$route_type, $entity_type_id, $entity_route_type] = array_pad(explode('.', $this->currentRouteMatch->getRouteName()), 3, NULL);
     if ($route_type !== 'entity' || $entity_route_type !== 'canonical') {
       return FALSE;
     }
diff --git a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e7d77e07a4bae0d123ad72ef719a46ddf34f036
--- /dev/null
+++ b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/FaqFullTestEntity.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\json_ld_schema_test_sources\Plugin\JsonLdEntity;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\json_ld_schema\Entity\JsonLdEntityBase;
+use Spatie\SchemaOrg\FAQPage;
+use Spatie\SchemaOrg\Schema;
+
+/**
+ * Test markup is properly escaped.
+ *
+ * @JsonLdEntity(
+ *   label = "FAQ Full Test Entity",
+ *   id = "faq_full_test",
+ * )
+ */
+class FaqFullTestEntity extends JsonLdEntityBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isApplicable(EntityInterface $entity, $view_mode) {
+    return $entity->getEntityTypeId() === 'node' && $entity->bundle() === 'faq' && $view_mode === 'full';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getData(EntityInterface $entity, $view_mode): FAQPage {
+    return Schema::FAQPage()
+      ->mainEntity([
+        Schema::Question()
+          ->name("question")
+          ->acceptedAnswer(
+            Schema::Answer()->text("<ul><li><a href='http://example.org'>answer!</a></li></ul>"),
+        ),
+      ]);
+  }
+
+}
diff --git a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php
index 995d6a15dc84ab43aec9ab25c64f0b313c6d43e6..3c3f874129249ea23ae85676b50b8148463fabaa 100644
--- a/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php
+++ b/tests/module/json_ld_schema_test_sources/src/Plugin/JsonLdEntity/NodeFullTestEntity.php
@@ -22,7 +22,7 @@ class NodeFullTestEntity extends JsonLdEntityBase {
    * {@inheritdoc}
    */
   public function isApplicable(EntityInterface $entity, $view_mode) {
-    return $entity->getEntityTypeId() === 'node' && $view_mode === 'full';
+    return $entity->getEntityTypeId() === 'node' && $entity->bundle() === 'example' && $view_mode === 'full';
   }
 
   /**
diff --git a/tests/src/Functional/JsonLdEntityTest.php b/tests/src/Functional/JsonLdEntityTest.php
index 9ce4384e7feac515b63372fdd3f82a8fd89a53f5..edcd0296f2537dbe1e4ee86ba9247f7950859249 100644
--- a/tests/src/Functional/JsonLdEntityTest.php
+++ b/tests/src/Functional/JsonLdEntityTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\json_ld_schema\Functional;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\json_ld_schema\Traits\JsonLdTestTrait;
 
 /**
  * Test the rendering of JSON LD scripts.
@@ -13,6 +14,8 @@ use Drupal\Tests\BrowserTestBase;
  */
 class JsonLdEntityTest extends BrowserTestBase {
 
+  use JsonLdTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -29,7 +32,7 @@ class JsonLdEntityTest extends BrowserTestBase {
   /**
    * Test the rendering of JSON LD and the integration with render cache.
    */
-  public function testJsonLdEntity() {
+  public function testJsonLdEntity(): void {
     NodeType::create([
       'type' => 'example',
       'label' => 'Example',
@@ -41,8 +44,9 @@ class JsonLdEntityTest extends BrowserTestBase {
     ]);
     $node->save();
 
-    $this->drupalGet($node->tourl());
-    $this->assertStringContainsString('"@type":"Brewery","name":"Example Node"', $this->getSession()->getPage()->getHtml());
+    $this->drupalGet($node->toUrl());
+    $schema = $this->getSchemaValues('Brewery');
+    $this->assertEquals('Example Node', $schema['name']);
     $this->assertBreadcrumbs();
     $this->assertOrganization();
 
@@ -51,22 +55,42 @@ class JsonLdEntityTest extends BrowserTestBase {
     // Change the low rating and revisit the page to ensure "1" was render
     // cached.
     $this->setRatingLow(2);
-    $this->drupalGet($node->tourl());
+    $this->drupalGet($node->toUrl());
     $this->assertRating(1);
 
     // Since query args were added as cache context, visit the node with the
     // high rating flag set.
-    $this->drupalGet($node->tourl(), ['query' => ['star_rating' => 'high']]);
+    $this->drupalGet($node->toUrl(), ['query' => ['star_rating' => 'high']]);
     $this->assertRating(5);
   }
 
+  /**
+   * Test the rendering of JSON LD with markup is properly escaped.
+   */
+  public function testMarkupEscaped(): void {
+    NodeType::create([
+      'type' => 'faq',
+      'label' => 'Faq',
+    ])->save();
+
+    $node = Node::create([
+      'type' => 'faq',
+      'title' => 'Example FAQ',
+    ]);
+    $node->save();
+
+    $this->drupalGet($node->toUrl());
+    $schema = $this->getSchemaValues('FAQPage');
+    $this->assertEquals("<ul><li><a href='http://example.org'>answer!</a></li></ul>", $schema['mainEntity'][0]['acceptedAnswer']['text']);
+  }
+
   /**
    * Set the high rating.
    *
    * @param int $rating
    *   The low rating.
    */
-  protected function setRatingHigh($rating) {
+  protected function setRatingHigh(int $rating): void {
     \Drupal::state()->set('json_ld_entity_test_rating_high', $rating);
   }
 
@@ -76,7 +100,7 @@ class JsonLdEntityTest extends BrowserTestBase {
    * @param int $rating
    *   The low rating.
    */
-  protected function setRatingLow($rating) {
+  protected function setRatingLow(int $rating): void {
     \Drupal::state()->set('json_ld_entity_test_rating_low', $rating);
   }
 
@@ -86,22 +110,31 @@ class JsonLdEntityTest extends BrowserTestBase {
    * @param int $rating
    *   The rating.
    */
-  protected function assertRating($rating) {
-    $this->assertStringContainsString('{"@type":"AggregateRating","ratingValue":' . $rating . '}', $this->getSession()->getPage()->getHtml());
+  protected function assertRating(int $rating): void {
+    $schema = $this->getSchemaValues('Brewery');
+    $this->assertEquals($rating, $schema['aggregateRating'][0]['ratingValue']);
   }
 
   /**
    * Assert the ld+json for Breadcrumbs are correct.
    */
-  protected function assertBreadcrumbs() {
-    $this->assertStringContainsString('"@context":"https:\/\/schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home"', $this->getSession()->getPage()->getHtml());
+  protected function assertBreadcrumbs(): void {
+    $schema = $this->getSchemaValues('BreadcrumbList');
+    $this->assertEquals([
+      ['@type' => 'ListItem', 'position' => 1, 'name' => 'Home'],
+    ], $schema['itemListElement']);
   }
 
   /**
    * Assert the ld+json for Organization are correct.
    */
-  protected function assertOrganization() {
-    $this->assertStringContainsString('"@context":"https:\/\/schema.org","@type":"Organization","url":"http:\/\/www.example.com","logo":"http:\/\/www.example.com\/logo.jpg"', $this->getSession()->getPage()->getHtml());
+  protected function assertOrganization(): void {
+    $schema = $this->getSchemaValues('Organization');
+    $this->assertEquals([
+      '@type' => 'Organization',
+      'url' => 'http://www.example.com',
+      'logo' => 'http://www.example.com/logo.jpg',
+    ], $schema);
   }
 
 }
diff --git a/tests/src/Functional/JsonLdSourceTest.php b/tests/src/Functional/JsonLdSourceTest.php
index 62d1f0349793e84368c29e97fd4ba1e77242cea5..81795f9e2474f39768891eefe96d35202c924cbc 100644
--- a/tests/src/Functional/JsonLdSourceTest.php
+++ b/tests/src/Functional/JsonLdSourceTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\json_ld_schema\Functional;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\json_ld_schema\Traits\JsonLdTestTrait;
 
 /**
  * Test the rendering of JSON LD scripts.
@@ -13,6 +14,8 @@ use Drupal\Tests\BrowserTestBase;
  */
 class JsonLdSourceTest extends BrowserTestBase {
 
+  use JsonLdTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -31,10 +34,11 @@ class JsonLdSourceTest extends BrowserTestBase {
    */
   public function testRendering() {
     $this->drupalGet('<front>');
-    $html = $this->getSession()->getPage()->getHtml();
-    $this->assertStringContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Foo"}</script>', $html);
-    $this->assertStringContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Bar"}</script>', $html);
-    $this->assertStringNotContainsString('<script type="application/ld+json">{"@context":"https:\/\/schema.org","@type":"Thing","name":"Baz"}</script>', $html);
+    $schema = $this->getSchemaValues('Thing', TRUE);
+    $this->assertEquals([
+      ['@type' => 'Thing', 'name' => 'Bar'],
+      ['@type' => 'Thing', 'name' => 'Foo'],
+    ], $schema);
   }
 
   /**
diff --git a/tests/src/Traits/JsonLdTestTrait.php b/tests/src/Traits/JsonLdTestTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..ccaada9f6418b26574b99c670353ed177f4f8293
--- /dev/null
+++ b/tests/src/Traits/JsonLdTestTrait.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tests\json_ld_schema\Traits;
+
+use Drupal\Component\Serialization\Json;
+
+/**
+ * Helper functions.
+ */
+trait JsonLdTestTrait {
+
+  /**
+   * Reads and decodes json schema data from the page.
+   */
+  protected function getSchemaValues(string $type = NULL, bool $all = FALSE): array {
+    $schemas = $this->getSession()->getPage()->findAll('xpath', '//script[@type="application/ld+json"]');
+    $return = [];
+    foreach ($schemas as $schema) {
+      $this->assertNotEmpty($schema);
+      $json = Json::decode($schema->getHtml());
+      $this->assertNotEmpty($json);
+      unset($json['@context']);
+      if (!$type) {
+        if (!$all) {
+          return $json;
+        }
+        $return[] = $json;
+      }
+
+      if ($json['@type'] === $type) {
+        if (!$all) {
+          return $json;
+        }
+        $return[] = $json;
+      }
+    }
+    return $return;
+  }
+
+}