diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
index 84556f8be0..0e87a4331d 100644
--- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
+++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
@@ -21,7 +21,7 @@
  *   description = @Translation("Stores a URL string, optional varchar link text, and optional blob of attributes to assemble a link."),
  *   default_widget = "link_default",
  *   default_formatter = "link",
- *   constraints = {"LinkType" = {}, "LinkAccess" = {}, "LinkExternalProtocols" = {}, "LinkNotExistingInternal" = {}}
+ *   constraints = {"LinkType" = {}, "LinkAccess" = {}, "LinkExternalProtocols" = {}, "LinkNotExistingInternal" = {}, "LinkUriValid" = {}}
  * )
  */
 class LinkItem extends FieldItemBase implements LinkItemInterface {
diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraint.php
new file mode 100644
index 0000000000..df402339ec
--- /dev/null
+++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraint.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\link\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraints\Url;
+
+/**
+ * Defines a validation constraint for invalid characters in the URI.
+ *
+ * @Constraint(
+ *   id = "LinkUriValid",
+ *   label = @Translation("No invalid characters in the URI field", context =
+ *   "Validation"),
+ * )
+ */
+class LinkUriValidConstraint extends Url {
+
+  public $message = "The path/URL '%value' contains invalid characters.";
+
+}
diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraintValidator.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraintValidator.php
new file mode 100644
index 0000000000..b194e2ac45
--- /dev/null
+++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkUriValidConstraintValidator.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\link\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\Url;
+use Symfony\Component\Validator\Constraints\UrlValidator;
+
+/**
+ * Validates the LinkUriValid constraint.
+ */
+class LinkUriValidConstraintValidator extends UrlValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    if (isset($value)) {
+      try {
+        /** @var \Drupal\Core\Url $urlObject */
+        $urlObject = $value->getUrl();
+        $url = $urlObject->setAbsolute()->toString();
+        // LinkExternalProtocolsConstraintValidator will deal with external
+        // URLs with invalid protocols.
+        $parts = parse_url($url) + ['scheme' => ''];
+        if (!$urlObject->isExternal() || in_array($parts['scheme'], $constraint->protocols)) {
+          parent::validate($url, $constraint);
+        }
+        if ($parts['scheme'] === 'mailto' && !filter_var($parts['path'], FILTER_VALIDATE_EMAIL)) {
+          $this->context->buildViolation($constraint->message)
+            ->setParameter('{{ value }}', $this->formatValue($value))
+            ->setCode(Url::INVALID_URL_ERROR)
+            ->addViolation();
+        }
+      }
+      catch (\Exception $e) {
+        // This is not our job, it can be invalid internal path and more.
+      }
+    }
+  }
+
+}
diff --git a/core/modules/link/tests/src/Functional/LinkFieldTest.php b/core/modules/link/tests/src/Functional/LinkFieldTest.php
index b78fe175bc..f764b91ace 100644
--- a/core/modules/link/tests/src/Functional/LinkFieldTest.php
+++ b/core/modules/link/tests/src/Functional/LinkFieldTest.php
@@ -148,10 +148,6 @@ public function testURLValidation() {
       'route:<nolink>' => '&lt;nolink&gt;',
       '<none>' => '&lt;none&gt;',
 
-      // Query string and fragment.
-      '?example=llama' => '?example=llama',
-      '#example' => '#example',
-
       // Entity reference autocomplete value.
       $node->label() . ' (1)' => $node->label() . ' (1)',
       // Entity URI displayed as ER autocomplete value when displayed in a form.
@@ -177,6 +173,10 @@ public function testURLValidation() {
       'entity:non_existing_entity_type/yar' => $validation_error_1,
       // URI for an entity that doesn't exist, with an invalid ID.
       'entity:user/invalid-parameter' => $validation_error_1,
+
+      // Query string and fragment.
+      '?example=llama' => '?example=llama',
+      '#example' => '#example',
     ];
 
     // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC.
diff --git a/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php b/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php
index aa0c849ab0..1483cfed6b 100644
--- a/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php
+++ b/core/modules/link/tests/src/Kernel/LinkItemUrlValidationTest.php
@@ -38,7 +38,9 @@ public function testExternalLinkValidation() {
           if (strpos($error_msg, '%')) {
             $error_msg = sprintf($error_msg, $value);
           }
-          $this->assertEquals($error_msg, $violations[$i++]->getMessage());
+          $msg = $violations[$i++]->getMessage();
+          $actual_msg = htmlspecialchars_decode(strip_tags($msg->render()), ENT_QUOTES);
+          $this->assertEquals($error_msg, $actual_msg);
         }
       }
     }
@@ -54,6 +56,7 @@ public function testExternalLinkValidation() {
   protected function getTestLinks() {
     $violation_0 = "The path '%s' is invalid.";
     $violation_1 = 'This value should be of the correct primitive type.';
+    $violation_2 = "The path/URL '\"%s\"' contains invalid characters.";
     return [
       ['invalid://not-a-valid-protocol', [$violation_0]],
       ['http://www.example.com/', []],
@@ -88,7 +91,7 @@ protected function getTestLinks() {
       ["http://foo.com/blah_(wikipedia)#cite-1", []],
       ["http://foo.com/blah_(wikipedia)_blah#cite-1", []],
       // The following invalid URLs produce false positives.
-      ["http://foo.com/unicode_(✪)_in_parens", []],
+      ["http://foo.com/unicode_(✪)_in_parens", [$violation_2]],
       ["http://foo.com/(something)?after=parens", []],
       ["http://☺.damowmow.com/", []],
       ["http://code.google.com/events/#&product=browser", []],
@@ -97,10 +100,10 @@ protected function getTestLinks() {
       ["http://foo.bar/?q=Test%20URL-encoded%20stuff", []],
       ["http://مثال.إختبار", []],
       ["http://例子.测试", []],
-      ["http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", []],
+      ["http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", [$violation_2]],
       ["http://1337.net", []],
       ["http://a.b-c.de", []],
-      ["radar://1234", [$violation_0]],
+      ["rdar://1234", [$violation_0]],
       ["h://test", [$violation_0]],
       ["ftps://foo.bar/", [$violation_0]],
       // Use invalid URLS from
