diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php
index a9ade9e..5114005 100644
--- a/core/modules/file/src/Entity/File.php
+++ b/core/modules/file/src/Entity/File.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\file\FileInterface;
+use Drupal\file\FileUriItem;
 use Drupal\user\UserInterface;
 
 /**
@@ -243,7 +244,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setLabel(t('Filename'))
       ->setDescription(t('Name of the file with no path components.'));
 
-    $fields['uri'] = BaseFieldDefinition::create('uri')
+    $fields['uri'] = BaseFieldDefinition::create('file_uri')
       ->setLabel(t('URI'))
       ->setDescription(t('The URI to access the file (either local or remote).'))
       ->setSetting('max_length', 255)
diff --git a/core/modules/file/src/FileUrl.php b/core/modules/file/src/FileUrl.php
new file mode 100644
index 0000000..0a39dfc
--- /dev/null
+++ b/core/modules/file/src/FileUrl.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\file;
+
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * Computed file URL property class.
+ */
+class FileUrl extends TypedData {
+
+  /**
+   * Cached URL.
+   *
+   * @var string|null
+   */
+  protected $url = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    if ($this->url !== NULL) {
+      return $this->url;
+    }
+
+    $uri = $this->getParent()->getEntity()->getFileUri();
+    $this->url = file_url_transform_relative(file_create_url($uri));
+
+    return $this->url;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value, $notify = TRUE) {
+    $this->url = $value;
+
+    // Notify the parent of any changes.
+    if ($notify && isset($this->parent)) {
+      $this->parent->onChange($this->name);
+    }
+  }
+
+}
diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php
new file mode 100644
index 0000000..575b3d6
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\file\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\file\FileUrl;
+
+/**
+ * File specific plugin implementation of a URI Item to provide a full URL.
+ *
+ * @FieldType(
+ *   id = "file_uri",
+ *   label = @Translation("File URI"),
+ *   description = @Translation("An entity field containing a URI and URL for a file path."),
+ *   no_ui = TRUE,
+ *   default_formatter = "uri_link",
+ *   default_widget = "uri",
+ * )
+ */
+class FileUriItem extends UriItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties = parent::propertyDefinitions($field_definition);
+
+    $properties['url'] = DataDefinition::create('uri')
+      ->setLabel(t('Full URL'))
+      ->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
+      ->setComputed(TRUE)
+      ->setInternal(FALSE)
+      ->setClass(FileUrl::class);
+
+    return $properties;
+  }
+
+}
diff --git a/core/modules/file/tests/src/Kernel/FileUriItemTest.php b/core/modules/file/tests/src/Kernel/FileUriItemTest.php
new file mode 100644
index 0000000..c42591b
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/FileUriItemTest.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\Tests\file\Kernel;
+use Drupal\file\Entity\File;
+
+/**
+ * Custom URI field item test.
+ *
+ * @group file
+ */
+class FileUriItemTest extends FileManagedUnitTestBase {
+
+  /**
+   * Tests the file entity override of the URI field.
+   */
+  public function testCustomFileUriField() {
+    $uri = 'public://druplicon.txt';
+
+    // Create a new file entity.
+    $file = File::create([
+      'uid' => 1,
+      'filename' => 'druplicon.txt',
+      'uri' => $uri,
+      'filemime' => 'text/plain',
+      'status' => FILE_STATUS_PERMANENT,
+    ]);
+    file_put_contents($file->getFileUri(), 'hello world');
+
+    $file->save();
+
+    $this->assertSame($uri, $file->uri->value);
+    $this->assertSame(file_create_url($uri), $file->uri->url);
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/FileUrlTest.php b/core/modules/file/tests/src/Unit/FileUrlTest.php
new file mode 100644
index 0000000..ec0f22c
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/FileUrlTest.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\Tests\file\Unit;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\file\FileUrl;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\file\FileUrl
+ *
+ * @group file
+ */
+class FileUrlTest extends UnitTestCase {
+
+  /**
+   * The test URL to use.
+   *
+   * @var string
+   */
+  protected $testUrl = 'https://localhost.test';
+
+  /**
+   * @covers ::getValue
+   */
+  public function testGetValue() {
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->url()
+      ->willReturn($this->testUrl);
+
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->getEntity()
+      ->shouldBeCalledTimes(1)
+      ->willReturn($entity->reveal());
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+    // Do this a second time to confirm the same value is returned but the value
+    // isn't retrieved from the parent entity again.
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+  }
+
+  /**
+   * @covers ::setValue
+   */
+  public function testSetValue() {
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->onChange('test')
+      ->shouldBeCalled();
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    // Setting the value should explicitly should mean the parent entity is
+    // never called into.
+    $typed_data->setValue($this->testUrl);
+
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+    // Do this a second time to confirm the same value is returned but the value
+    // isn't retrieved from the parent entity again.
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+  }
+
+  /**
+   * @covers ::setValue
+   */
+  public function testSetValueNoNotify() {
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->onChange('test')
+      ->shouldNotBeCalled();
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    // Setting the value should explicitly should mean the parent entity is
+    // never called into.
+    $typed_data->setValue($this->testUrl, FALSE);
+  }
+
+}
diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
index ec870e9..1b3ee6f 100644
--- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
@@ -47,17 +47,6 @@ public function __construct(EntityManagerInterface $entity_manager, ClientInterf
   /**
    * {@inheritdoc}
    */
-  public function normalize($entity, $format = NULL, array $context = []) {
-    $data = parent::normalize($entity, $format, $context);
-    // Replace the file url with a full url for the file.
-    $data['uri'][0]['value'] = $this->getEntityUri($entity);
-
-    return $data;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function denormalize($data, $class, $format = NULL, array $context = []) {
     $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
 
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
index c71b54e..66e1cc0 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
@@ -86,11 +86,6 @@ protected function getExpectedNormalizedEntity() {
               ],
             ],
             'lang' => 'en',
-            'uri' => [
-              [
-                'value' => $file->url(),
-              ],
-            ],
             'uuid' => [
               [
                 'value' => $file->uuid(),
@@ -126,11 +121,6 @@ protected function getExpectedNormalizedEntity() {
               ],
             ],
             'lang' => 'en',
-            'uri' => [
-              [
-                'value' => $thumbnail->url(),
-              ],
-            ],
             'uuid' => [
               [
                 'value' => $thumbnail->uuid(),
diff --git a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
index c4b67f5..355c09d 100644
--- a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
@@ -44,7 +44,10 @@ public function testNormalize() {
 
     $expected_array = [
       'uri' => [
-        ['value' => file_create_url($file->getFileUri())],
+        [
+          'value' => $file->getFileUri(),
+          'url' => file_url_transform_relative(file_create_url($file->getFileUri())),
+        ],
       ],
     ];
 
