diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
index 09b4b64..c1637e9 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -2,10 +2,9 @@
 
 namespace Drupal\rest\Plugin\rest\resource;
 
-use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
-use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+use Drupal\serialization\Exception\EntityValidationException;
 
 /**
  * @internal
@@ -19,7 +18,7 @@
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity to validate.
    *
-   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
+   * @throws \Drupal\serialization\Exception\EntityValidationException
    *   If validation errors are found.
    */
   protected function validate(EntityInterface $entity) {
@@ -34,13 +33,7 @@ protected function validate(EntityInterface $entity) {
     $violations->filterByFieldAccess();
 
     if ($violations->count() > 0) {
-      $message = "Unprocessable Entity: validation failed.\n";
-      foreach ($violations as $violation) {
-        // We strip every HTML from the error message to have a nicer to read
-        // message on REST responses.
-        $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n";
-      }
-      throw new UnprocessableEntityHttpException($message);
+      throw new EntityValidationException($violations, "Validation of the {$entity->getEntityTypeId()} entity failed");
     }
   }
 
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 713d403..eaea09b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -696,7 +696,14 @@ public function testPost() {
     $response = $this->request('POST', $url, $request_options);
     $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
     $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    $entity_type_id = static::$entityTypeId;
+    $error = [
+      'message' => "Validation of the $entity_type_id entity failed",
+      'errors' => [
+        $label_field => ["$label_field_capitalized: this field cannot hold more than 1 values."],
+      ],
+    ];
+    $this->assertResourceResponse(422, json_encode($error), $response);
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
@@ -704,7 +711,13 @@ public function testPost() {
 
     // DX: 422 when invalid entity: UUID field too long.
     $response = $this->request('POST', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+    $error = [
+      'message' => "Validation of the $entity_type_id entity failed",
+      'errors' => [
+        'uuid.0.value' => ['UUID: may not be longer than 128 characters.'],
+      ],
+    ];
+    $this->assertResourceResponse(422, json_encode($error), $response);
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
@@ -885,7 +898,13 @@ public function testPatch() {
     $response = $this->request('PATCH', $url, $request_options);
     $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
     $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    $error = [
+      'message' => "Validation of the {$this->entity->getEntityTypeId()} entity failed",
+      'errors' => [
+        $label_field => ["$label_field_capitalized: this field cannot hold more than 1 values."],
+      ],
+    ];
+    $this->assertResourceResponse(422, json_encode($error), $response);
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
index 04fe435..a554894 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
@@ -163,7 +163,13 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
 
     // DX: 422 when changing email without providing the password.
     $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
+    $error = [
+      'message' => "Validation of the {$this->entity->getEntityTypeId()} entity failed",
+      'errors' => [
+        'mail' => "Your current password is missing or incorrect; it's required to change the Email.",
+      ],
+    ];
+    $this->assertResourceErrorResponse(422, json_encode($error), $response);
 
 
     $normalization['pass'] = [['existing' => 'wrong']];
@@ -171,7 +177,13 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
 
     // DX: 422 when changing email while providing a wrong password.
     $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
+    $error = [
+      'message' => "Validation of the {$this->entity->getEntityTypeId()} entity failed",
+      'errors' => [
+        'mail' => "Your current password is missing or incorrect; it's required to change the Email.",
+      ],
+    ];
+    $this->assertResourceErrorResponse(422, json_encode($error), $response);
 
 
     $normalization['pass'] = [['existing' => $this->account->passRaw]];
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 3e47b00..3d74e19 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -64,6 +64,10 @@ services:
     class: Drupal\serialization\Normalizer\TypedDataNormalizer
     tags:
       - { name: normalizer }
+  serializer.normalizer.entity_validation_exception:
+    class: \Drupal\serialization\Normalizer\EntityValidationExceptionNormalizer
+    tags:
+      - { name: normalizer }
   serializer.encoder.json:
     class: Drupal\serialization\Encoder\JsonEncoder
     tags:
diff --git a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
index a1e7bad..b31bdb9 100644
--- a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
+++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php
@@ -62,13 +62,19 @@ protected static function getPriority() {
    *   The event to process.
    */
   public function on4xx(GetResponseForExceptionEvent $event) {
-    /** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
+    /** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface|\Exception $exception */
     $exception = $event->getException();
     $request = $event->getRequest();
 
     $format = $request->getRequestFormat();
-    $content = ['message' => $event->getException()->getMessage()];
-    $encoded_content = $this->serializer->serialize($content, $format);
+
+    if ($this->serializer->supportsNormalization($exception, $format)) {
+      $encoded_content = $this->serializer->serialize($exception, $format);
+    }
+    else {
+      $content = ['message' => $exception->getMessage()];
+      $encoded_content = $this->serializer->serialize($content, $format);
+    }
     $headers = $exception->getHeaders();
 
     // Add the MIME type from the request to send back in the header.
diff --git a/core/modules/serialization/src/Exception/EntityValidationException.php b/core/modules/serialization/src/Exception/EntityValidationException.php
new file mode 100644
index 0000000..caf5de7
--- /dev/null
+++ b/core/modules/serialization/src/Exception/EntityValidationException.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\serialization\Exception;
+
+use Drupal\Core\Entity\EntityConstraintViolationListInterface;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * An exception thrown when entity validation failed.
+ *
+ * This exception is normalized/encoded into a nice machine readable error
+ * message.
+ *
+ * @see \Drupal\serialization\Normalizer\EntityValidationExceptionNormalizer
+ */
+class EntityValidationException extends UnprocessableEntityHttpException {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityConstraintViolationListInterface
+   */
+  protected $violations;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityConstraintViolationListInterface $violations, $message = NULL, \Exception $previous = NULL, $code = 0) {
+    parent::__construct($message, $previous, $code);
+
+    $this->violations = $violations;
+  }
+
+  /**
+   * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
+   */
+  public function getViolations() {
+    return $this->violations;
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/EntityValidationExceptionNormalizer.php b/core/modules/serialization/src/Normalizer/EntityValidationExceptionNormalizer.php
new file mode 100644
index 0000000..0fccf30
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/EntityValidationExceptionNormalizer.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\serialization\Exception\EntityValidationException;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * Normalizes entity validation errors.
+ */
+class EntityValidationExceptionNormalizer extends NormalizerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = EntityValidationException::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    $result = [];
+    if ($object instanceof EntityValidationException) {
+      $violations = $object->getViolations();
+      $result['message'] = $object->getMessage();
+
+      // Group violations by field path.
+      $result['errors'] = array_reduce(iterator_to_array($violations), function (array $carry, ConstraintViolation $violation) {
+        $carry[$violation->getPropertyPath()][] = PlainTextOutput::renderFromHtml($violation->getMessage());
+        return $carry;
+      }, []);
+
+    }
+
+    return $result;
+  }
+
+}
diff --git a/core/tests/README.md b/core/tests/README.md
index 566fa23..a454c7e 100644
--- a/core/tests/README.md
+++ b/core/tests/README.md
@@ -1,4 +1,4 @@
-# Running tests
+# Runnin testt
 
 ## Functional tests
 
