 jsonapi.info.yml                                   |  2 +
 jsonapi.services.yml                               |  2 +-
 src/EventSubscriber/ResourceResponseSubscriber.php | 84 +++++++++++++++++++---
 .../ResourceResponseSubscriberTest.php             | 24 +++++--
 4 files changed, 97 insertions(+), 15 deletions(-)

diff --git a/jsonapi.info.yml b/jsonapi.info.yml
index cd3b601..ea504a4 100644
--- a/jsonapi.info.yml
+++ b/jsonapi.info.yml
@@ -6,3 +6,5 @@ package: Web services
 dependencies:
   - drupal:system (>=8.2)
   - serialization
+test_dependencies:
+  - schemata:schemata_json_schema
diff --git a/jsonapi.services.yml b/jsonapi.services.yml
index 2fda8c8..4af54b4 100644
--- a/jsonapi.services.yml
+++ b/jsonapi.services.yml
@@ -137,7 +137,7 @@ services:
     class: Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@serializer', '@renderer', '@logger.channel.jsonapi']
+    arguments: ['@serializer', '@renderer', '@logger.channel.jsonapi', '@module_handler']
 
   # Deprecated services.
   serializer.normalizer.htt_exception.jsonapi:
diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php
index ce8d1f2..ffb5493 100644
--- a/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\EventSubscriber;
 
+use Drupal\Core\Extension\ModuleHandlerInterface;
 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;
@@ -58,6 +60,13 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
   protected $logger;
 
   /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
    * Constructs a ResourceResponseSubscriber object.
    *
    * @param \Symfony\Component\Serializer\SerializerInterface $serializer
@@ -66,11 +75,14 @@ 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) {
+  public function __construct(SerializerInterface $serializer, RendererInterface $renderer, LoggerInterface $logger, ModuleHandlerInterface $module_handler) {
     $this->serializer = $serializer;
     $this->renderer = $renderer;
     $this->logger = $logger;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
@@ -90,7 +102,7 @@ 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');
+    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 +183,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 (!class_exists(Validator::class)) {
       return TRUE;
     }
     // Do not use Json::decode here since it coerces the response into an
@@ -186,12 +200,65 @@ 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->moduleHandler->moduleExists('schemata_json_schema')) {
+      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);
+    // Check if the response is for a collection. We need to validate each
+    // resource object against the schema.
+    $is_collection = strpos(
+      $request->attributes->get(RouteObjectInterface::ROUTE_NAME),
+      '.collection'
+    ) !== FALSE;
+    $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');
+    $serializer = \Drupal::service('serializer');
+    $generic_jsonapi_schema = $schema_factory->create($entity_type_id, $bundle);
+    $format = $output_format . ':' . $described_format;
+    $output = $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) {
+    $validator = new Validator();
+
+    $validator->check($response_data, $schema);
+    $is_valid = $validator->isValid();
+    if (!$is_valid) {
       $this->logger->debug('Response failed validation: @data', [
         '@data' => Json::encode($response_data),
       ]);
@@ -199,8 +266,7 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
         '@errors' => Json::encode($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..8bc893a 100644
--- a/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
+++ b/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
@@ -2,11 +2,15 @@
 
 namespace Drupal\Tests\jsonapi\Unit\EventSubscriber;
 
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber;
 use Drupal\rest\ResourceResponse;
 use Drupal\Tests\UnitTestCase;
 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;
 
 /**
@@ -19,10 +23,14 @@ class ResourceResponseSubscriberTest extends UnitTestCase {
    * @covers ::validateResponse
    */
   public function testValidateResponse() {
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class);
+    $module_handler->moduleExists('schemata_json_schema')
+      ->willReturn(FALSE);
     $resource_response_subscriber = new ResourceResponseSubscriber(
       $this->prophesize(Serializer::class)->reveal(),
       $this->prophesize(RendererInterface::class)->reveal(),
-      $this->prophesize(LoggerInterface::class)->reveal()
+      $this->prophesize(LoggerInterface::class)->reveal(),
+      $module_handler->reveal()
     );
 
     // Check that the validation class is enabled.
@@ -48,10 +56,16 @@ class ResourceResponseSubscriberTest extends UnitTestCase {
   }
 }
 EOD;
+    $request = new Request();
+    $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'jsonapi.node--article.individual');
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, (new Route('/jsonapi/node/article/{node}'))->setRequirements([
+      '_entity_type' => 'node',
+      '_bundle' => 'article',
+    ]));
     $response = new ResourceResponse();
     $response->setContent($json);
     $this->assertFalse(
-      $validate_response->invoke($resource_response_subscriber, $response),
+      $validate_response->invoke($resource_response_subscriber, $response, $request),
       'Response validation failed to flag an invalid response.'
     );
 
@@ -72,7 +86,7 @@ EOD;
     $response = new ResourceResponse();
     $response->setContent($json);
     $this->assertFalse(
-      $validate_response->invoke($resource_response_subscriber, $response),
+      $validate_response->invoke($resource_response_subscriber, $response, $request),
       'Response validation failed to flag an invalid response.'
     );
 
@@ -91,14 +105,14 @@ EOD;
 EOD;
     $response->setContent($json);
     $this->assertTrue(
-      $validate_response->invoke($resource_response_subscriber, $response),
+      $validate_response->invoke($resource_response_subscriber, $response, $request),
       'Response validation flagged a valid response.'
     );
 
     // Test validation of an empty response passes.
     $response = new ResourceResponse();
     $this->assertTrue(
-      $validate_response->invoke($resource_response_subscriber, $response),
+      $validate_response->invoke($resource_response_subscriber, $response, $request),
       'Response validation flagged a valid empty response.'
     );
 
