diff --git a/composer.json b/composer.json
index e54e7e5..ee8fa15 100644
--- a/composer.json
+++ b/composer.json
@@ -4,6 +4,7 @@
     "type": "drupal-module",
     "license": "GPL-2.0+",
     "require-dev": {
-        "justinrainbow/json-schema": "^4.1"
+        "justinrainbow/json-schema": "^4.1",
+        "drupal/schemata": "1.x-dev#8325d172e1d6880aa24073f8f751ef089282cf9a"
     }
 }
diff --git a/jsonapi.info.yml b/jsonapi.info.yml
index 3702756..94bf7e3 100644
--- a/jsonapi.info.yml
+++ b/jsonapi.info.yml
@@ -6,3 +6,5 @@ package: Web services
 dependencies:
   - drupal:system (>=8.3)
   - serialization
+test_dependencies:
+  - schemata:schemata_json_schema
diff --git a/jsonapi.services.yml b/jsonapi.services.yml
index e66e5b5..d698421 100644
--- a/jsonapi.services.yml
+++ b/jsonapi.services.yml
@@ -134,6 +134,9 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@serializer', '@renderer', '@logger.channel.jsonapi']
+    calls:
+      - [setValidator, []]
+      - [setSchemaFactory, ['@?schemata.schema_factory']]
 
   # Deprecated services.
   serializer.normalizer.htt_exception.jsonapi:
diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php
index 17b1327..31b253e 100644
--- a/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\EventSubscriber;
 
+use Drupal\schemata\SchemaFactory;
 use JsonSchema\Validator;
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\CacheableResponse;
@@ -10,6 +11,7 @@ use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\jsonapi\ResourceResponse;
 use Psr\Log\LoggerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@@ -21,6 +23,7 @@ use Symfony\Component\Serializer\SerializerInterface;
  * Response subscriber that serializes and removes ResourceResponses' data.
  *
  * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+ * @internal
  *
  * This is 99% identical to \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
  * but with a few differences:
@@ -57,6 +60,24 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
    */
   protected $logger;
 
+  /**
+   * The schema validator.
+   *
+   * This property will only be set if the validator library is available.
+   *
+   * @var \JsonSchema\Validator|NULL
+   */
+  protected $validator;
+
+  /**
+   * The schemata schema factory.
+   *
+   * This property will only be set if the schemata module is installed.
+   *
+   * @var \Drupal\schemata\SchemaFactory|NULL
+   */
+  protected $schemaFactory;
+
   /**
    * Constructs a ResourceResponseSubscriber object.
    *
@@ -66,6 +87,8 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
    *   The renderer.
    * @param \Psr\Log\LoggerInterface $logger
    *   The JSON API logger channel.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
    */
   public function __construct(SerializerInterface $serializer, RendererInterface $renderer, LoggerInterface $logger) {
     $this->serializer = $serializer;
@@ -73,6 +96,27 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
     $this->logger = $logger;
   }
 
+  /**
+   * Sets the validator service if available.
+   */
+  public function setValidator(Validator $validator = NULL) {
+    if ($validator) {
+      $this->validator = $validator;
+    }
+    elseif (class_exists(Validator::class)) {
+      $this->validator = new Validator();
+    }
+  }
+
+  /**
+   * Injects the schema factory.
+   *
+   * @param \Drupal\schemata\SchemaFactory
+   */
+  public function setSchemaFactory(SchemaFactory $schema_factory) {
+    $this->schemaFactory = $schema_factory;
+  }
+
   /**
    * Serializes ResourceResponse responses' data, and removes that data.
    *
@@ -90,7 +134,16 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
     $this->renderResponseBody($request, $response, $this->serializer, $format);
     $event->setResponse($this->flattenResponse($response));
 
-    assert($this->validateResponse($event->getResponse()), 'A JSON API response failed validation (see the logs for details). Please report this in the issue queue on drupal.org');
+    $this->doValidateResponse($response, $request);
+  }
+
+  /**
+   * Wraps validation in an assert to prevent execution in production.
+   *
+   * @see self::validateResponse
+   */
+  public function doValidateResponse(Response $response, Request $request) {
+    assert($this->validateResponse($response, $request), 'A JSON API response failed validation (see the logs for details). Please report this in the issue queue on drupal.org');
   }
 
   /**
@@ -171,12 +224,14 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
    *
    * @param \Symfony\Component\HttpFoundation\Response $response
    *   The response to validate.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request containing info about what to validate.
    *
    * @return bool
    *   FALSE if the response failed validation, otherwise TRUE.
    */
-  protected function validateResponse(Response $response) {
-    if (!class_exists("\\JsonSchema\\Validator")) {
+  protected function validateResponse(Response $response, Request $request) {
+    if (!$this->validator) {
       return TRUE;
     }
     // Do not use Json::decode here since it coerces the response into an
@@ -186,21 +241,73 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
       return TRUE;
     }
 
-    $validator = new Validator();
     $schema_path = dirname(dirname(__DIR__)) . '/schema.json';
+    $generic_jsonapi_schema = (object) ['$ref' => 'file://' . $schema_path];
+    $is_valid = $this->validateSchema($generic_jsonapi_schema, $response_data);
+    if (!$is_valid) {
+      return FALSE;
+    }
+
+    if (!$this->schemaFactory) {
+      return TRUE;
+    }
+    // Get the schema for the current resource. For that we will need to
+    // introspect the request to find the entity type and bundle matched by the
+    // router.
+    $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
+    $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME);
+    // Check if the response is a collection, related, and/or relationship. We need to validate each
+    // collection resource object against the schema and skip related/relationships.
+    $is_related = strpos($route_name, '.related') !== FALSE;
+    $is_relationship = strpos($route_name, '.relationship') !== FALSE;
+    $is_collection = strpos($route_name, '.collection') !== FALSE;
+    if ($is_related || $is_relationship) {
+      return TRUE;
+    }
+    $entity_type_id = $route->getRequirement('_entity_type');
+    $bundle = $route->getRequirement('_bundle');
+    $output_format = 'schema_json';
+    $described_format = 'api_json';
 
-    $validator->check($response_data, (object) ['$ref' => 'file://' . $schema_path]);
+    $schema_factory = \Drupal::service('schemata.schema_factory');
+    $generic_jsonapi_schema = $schema_factory->create($entity_type_id, $bundle);
+    $format = $output_format . ':' . $described_format;
+    $output = $this->serializer->serialize($generic_jsonapi_schema, $format);
+    $specific_schema = json_decode($output);
 
-    if (!$validator->isValid()) {
+    // Iterate over each resource object and check the schema.
+    return array_reduce(
+      $is_collection ? $response_data->data : [$response_data->data],
+      function ($valid, $resource_object) use ($specific_schema) {
+        return $this->validateSchema($specific_schema, $resource_object) && $valid;
+      },
+      TRUE
+    );
+  }
+
+  /**
+   * Validates a string against a JSON Schema. It logs any possible errors.
+   *
+   * @param object $schema
+   *   The JSON Schema object.
+   * @param string $response_data
+   *   The JSON string to validate.
+   *
+   * @return bool
+   *   TRUE if the string is a valid instance of the schema. FALSE otherwise.
+   */
+  protected function validateSchema($schema, $response_data) {
+    $this->validator->check($response_data, $schema);
+    $is_valid = $this->validator->isValid();
+    if (!$is_valid) {
       $this->logger->debug('Response failed validation: @data', [
         '@data' => Json::encode($response_data),
       ]);
       $this->logger->debug('Validation errors: @errors', [
-        '@errors' => Json::encode($validator->getErrors()),
+        '@errors' => Json::encode($this->validator->getErrors()),
       ]);
     }
-
-    return $validator->isValid();
+    return $is_valid;
   }
 
   /**
diff --git a/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php b/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
index 823b11d..6b9ead2 100644
--- a/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
+++ b/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
@@ -6,7 +6,11 @@ use Drupal\Core\Render\RendererInterface;
 use Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber;
 use Drupal\rest\ResourceResponse;
 use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
 use Psr\Log\LoggerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
 use Symfony\Component\Serializer\Serializer;
 
 /**
@@ -16,30 +20,101 @@ use Symfony\Component\Serializer\Serializer;
 class ResourceResponseSubscriberTest extends UnitTestCase {
 
   /**
-   * @covers ::validateResponse
+   * The subscriber under test.
+   *
+   * @var \Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber
+   */
+  protected $subscriber;
+
+  /**
+   * {@inheritdoc}
    */
-  public function testValidateResponse() {
-    $resource_response_subscriber = new ResourceResponseSubscriber(
+  public function setUp() {
+    // Check that the validation class is available.
+    if (!class_exists("\\JsonSchema\\Validator")) {
+      $this->fail('The JSON Schema validator is missing. You can install it with `composer require justinrainbow/json-schema`.');
+    }
+
+    $subscriber = new ResourceResponseSubscriber(
       $this->prophesize(Serializer::class)->reveal(),
       $this->prophesize(RendererInterface::class)->reveal(),
       $this->prophesize(LoggerInterface::class)->reveal()
     );
+    $subscriber->setValidator();
+    $this->subscriber = $subscriber;
+  }
 
-    // Check that the validation class is enabled.
-    $this->assertTrue(
-      class_exists("\\JsonSchema\\Validator"),
-      'The JSON Schema validator is not present. Please make sure to install it using composer.'
+  /**
+   * @covers ::onResponse
+   */
+  public function testOnResponse() {
+    // In PHP 7 and above, we can test that validation is skipped in production. Below 7, these tests would always fail.
+    if (PHP_MAJOR_VERSION < 7) {
+      $this->markTestSkipped('Skipped because \'zend.assertions = Off\' is only possible in PHP 7 and above.');
+      return;
+    }
+
+    $request = $this->createRequest(
+      'jsonapi.node--article.individual',
+      '/jsonapi/node/article/{node}',
+      ['_entity_type' => 'node', '_bundle' => 'article']
     );
 
+    $response = $this->createResponse('{"data":null}');
+
+    // Capture the default assert settings.
+    $assert_default = ini_get('zend.assertions');
+
+    // The validator *should* be called when asserts are active.
+    $validator = $this->prophesize(\JsonSchema\Validator::class);
+    $validator->check(Argument::any(), Argument::any())->shouldBeCalled('Validation should be run when asserts are active.');
+    $validator->isValid()->willReturn(TRUE);
+    $this->subscriber->setValidator($validator->reveal());
+
+    // Ensure asset is active.
+    ini_set('zend.assertions', 1);
+    $this->subscriber->doValidateResponse($response, $request);
+
+    // The validator should *not* be called when asserts are inactive.
+    $validator = $this->prophesize(\JsonSchema\Validator::class);
+    $validator->check(Argument::any(), Argument::any())->shouldNotBeCalled('Validation should not be run when asserts are not active.');
+    $this->subscriber->setValidator($validator->reveal());
+
+    // Ensure asset is inactive.
+    ini_set('zend.assertions', 0);
+    $this->subscriber->doValidateResponse($response, $request);
+
+    // Reset the original assert values.
+    ini_set('zend.assertions', $assert_default);
+  }
+
+  /**
+   * @covers ::validateResponse
+   * @dataProvider validateResponseProvider
+   */
+  public function testValidateResponse($request, $response, $expected, $description) {
     // Expose protected ResourceResponseSubscriber::validateResponse() method.
-    $object = new \ReflectionObject($resource_response_subscriber);
-    $validate_response = $object->getMethod('validateResponse');
-    $validate_response->setAccessible(TRUE);
+    $object = new \ReflectionObject($this->subscriber);
+    $method = $object->getMethod('validateResponse');
+    $method->setAccessible(TRUE);
+
+    $this->assertSame($expected, $method->invoke($this->subscriber, $response, $request), $description);
+  }
 
-    // Test validation failure: no "type" in "data".
-    $json = <<<'EOD'
+  public function validateResponseProvider() {
+    $defaults = [
+      'route_name' => 'jsonapi.node--article.individual',
+      'route' => '/jsonapi/node/article/{node}',
+      'requirements' => ['_entity_type' => 'node', '_bundle' => 'article'],
+    ];
+
+    $test_data = [
+      // Test validation success.
+      [
+        'json' => <<<'EOD'
 {
   "data": {
+    "type": "node--article",
     "id": "4f342419-e668-4b76-9f87-7ce20c436169",
     "attributes": {
       "nid": "1",
@@ -47,61 +122,102 @@ class ResourceResponseSubscriberTest extends UnitTestCase {
     }
   }
 }
-EOD;
-    $response = new ResourceResponse();
-    $response->setContent($json);
-    $this->assertFalse(
-      $validate_response->invoke($resource_response_subscriber, $response),
-      'Response validation failed to flag an invalid response.'
-    );
-
-    // Test validation failure: no "data" and "errors" at the root level.
-    $json = <<<'EOD'
+EOD
+        ,
+        'expected' => TRUE,
+        'description' => 'Response validation flagged a valid response.',
+      ],
+      // Test validation failure: no "type" in "data".
+      [
+        'json' => <<<'EOD'
 {
   "data": {
-    "type": "node--article",
     "id": "4f342419-e668-4b76-9f87-7ce20c436169",
     "attributes": {
       "nid": "1",
       "uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
     }
-  },
-  "errors": [{}]
+  }
 }
-EOD;
-    $response = new ResourceResponse();
-    $response->setContent($json);
-    $this->assertFalse(
-      $validate_response->invoke($resource_response_subscriber, $response),
-      'Response validation failed to flag an invalid response.'
-    );
-
-    // Test validation success.
-    $json = <<<'EOD'
+EOD
+        ,
+        'expected' => FALSE,
+        'description' => 'Response validation failed to flag an invalid response.',
+      ],
+      // Test validation failure: "errors" at the root level.
+      [
+        'json' => <<<'EOD'
 {
   "data": {
-    "type": "node--article",
+  "type": "node--article",
     "id": "4f342419-e668-4b76-9f87-7ce20c436169",
     "attributes": {
-      "nid": "1",
+    "nid": "1",
       "uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
     }
-  }
+  },
+  "errors": [{}]
 }
-EOD;
-    $response->setContent($json);
-    $this->assertTrue(
-      $validate_response->invoke($resource_response_subscriber, $response),
-      'Response validation flagged a valid response.'
-    );
+EOD
+        ,
+        'expected' => FALSE,
+        'description' => 'Response validation failed to flag an invalid response.',
+      ],
+      // Test validation of an empty response passes.
+      [
+        'json' => NULL,
+        'expected' => TRUE,
+        'description' => 'Response validation flagged a valid empty response.',
+      ],
+      // Test validation fails on empty object.
+      [
+        'json' => '{}',
+        'expected' => FALSE,
+        'description' => 'Response validation flags empty array as invalid.',
+      ],
+    ];
 
-    // Test validation of an empty response passes.
-    $response = new ResourceResponse();
-    $this->assertTrue(
-      $validate_response->invoke($resource_response_subscriber, $response),
-      'Response validation flagged a valid empty response.'
-    );
+    $test_cases = array_map(function ($input) use ($defaults) {
+      list($json, $expected, $description, $route_name, $route, $requirements) = array_values($input + $defaults);
+      return [
+        $this->createRequest($route_name, $route, $requirements),
+        $this->createResponse($json),
+        $expected,
+        $description,
+      ];
+    }, $test_data);
 
+    return $test_cases;
+  }
+
+  /**
+   * Helper method to create a request object.
+   *
+   * @param $route_name string
+   * @param $route string
+   * @param $requirements array
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   */
+  protected function createRequest($route_name, $route, array $requirements = []) {
+    $request = new Request();
+    $request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name);
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, (new Route($route))->setRequirements($requirements));
+    return $request;
+  }
+
+  /**
+   * Helper method to create a resource response from arbitrary JSON.
+   *
+   * @param $json string|NULL
+   * @return \Drupal\rest\ResourceResponse
+   */
+  protected function createResponse($json = NULL) {
+    $response = new ResourceResponse();
+    if ($json) {
+      $response->setContent($json);
+    }
+    return $response;
   }
 
 }
