diff --git a/reviewboard/attachments/evolutions/__init__.py b/reviewboard/attachments/evolutions/__init__.py
index 29d418660a941a7ac9b44871be914539bd3054ef..81a97eb0e3e341b34eeb15f55ddda5bb6081ddd5 100644
--- a/reviewboard/attachments/evolutions/__init__.py
+++ b/reviewboard/attachments/evolutions/__init__.py
@@ -7,4 +7,5 @@ SEQUENCE = [
     'file_attachment_revision',
     'file_attachment_ownership',
     'file_attachment_uuid',
+    'file_attachment_extra_data',
 ]
diff --git a/reviewboard/attachments/evolutions/file_attachment_extra_data.py b/reviewboard/attachments/evolutions/file_attachment_extra_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..af053e8741c992f456ee30c02c4ca5955e61543a
--- /dev/null
+++ b/reviewboard/attachments/evolutions/file_attachment_extra_data.py
@@ -0,0 +1,13 @@
+"""Added FileAttachment.extra_data.
+
+Version Added:
+    6.0
+"""
+
+from django_evolution.mutations import AddField
+from djblets.db.fields import JSONField
+
+
+MUTATIONS = [
+    AddField('FileAttachment', 'extra_data', JSONField, null=True),
+]
diff --git a/reviewboard/attachments/forms.py b/reviewboard/attachments/forms.py
index baab3b27ac05963a83c795254d63798884d5ecba..082a2f7ca86b0dff37157135487d0751362f5b41 100644
--- a/reviewboard/attachments/forms.py
+++ b/reviewboard/attachments/forms.py
@@ -1,7 +1,9 @@
-from uuid import uuid4
+
 import os
+from uuid import uuid4
 
 from django import forms
+from djblets.db.fields.json_field import JSONFormField
 
 from reviewboard.attachments.mimetypes import get_uploaded_file_mimetype
 from reviewboard.attachments.models import (FileAttachment,
@@ -26,6 +28,9 @@ class UploadFileForm(forms.Form):
         queryset=FileAttachmentHistory.objects.all(),
         required=False)
 
+    #: Extra data as part of the file attachment.
+    extra_data = JSONFormField(required=False)
+
     def __init__(self, review_request, *args, **kwargs):
         """Initialize the form.
 
@@ -76,6 +81,7 @@ class UploadFileForm(forms.Form):
         """
         file_obj = self.files['path']
         caption = self.cleaned_data['caption'] or file_obj.name
+        extra_data = self.cleaned_data['extra_data']
 
         mimetype = get_uploaded_file_mimetype(file_obj)
         filename = get_unique_filename(file_obj.name)
@@ -116,6 +122,7 @@ class UploadFileForm(forms.Form):
             'attachment_revision': attachment_revision,
             'caption': '',
             'draft_caption': caption,
+            'extra_data': extra_data,
             'orig_filename': os.path.basename(file_obj.name),
             'mimetype': mimetype,
         }
@@ -146,6 +153,9 @@ class UploadUserFileForm(forms.Form):
     #: The file itself.
     path = forms.FileField(required=False)
 
+    #: Extra data as part of the file attachment.
+    extra_data = JSONFormField(required=False)
+
     def create(self, user, local_site=None):
         """Create a FileAttachment based on this form.
 
@@ -171,11 +181,13 @@ class UploadUserFileForm(forms.Form):
         if file_obj:
             mimetype = get_uploaded_file_mimetype(file_obj)
             filename = get_unique_filename(file_obj.name)
+            extra_data = self.cleaned_data['extra_data']
 
             attachment_kwargs.update({
                 'caption': self.cleaned_data['caption'] or file_obj.name,
                 'orig_filename': os.path.basename(file_obj.name),
                 'mimetype': mimetype,
+                'extra_data': extra_data,
             })
 
             file_attachment = FileAttachment(**attachment_kwargs)
@@ -201,6 +213,7 @@ class UploadUserFileForm(forms.Form):
         """
         caption = self.cleaned_data['caption']
         file_obj = self.files.get('path')
+        extra_data = self.cleaned_data['extra_data']
 
         if caption:
             file_attachment.caption = caption
@@ -211,6 +224,9 @@ class UploadUserFileForm(forms.Form):
             file_attachment.file.save(get_unique_filename(file_obj.name),
                                       file_obj, save=True)
 
+        if extra_data:
+            file_attachment.extra_data = extra_data
+
         file_attachment.save()
 
         return file_attachment
diff --git a/reviewboard/attachments/models.py b/reviewboard/attachments/models.py
index 83370530698eac32ebbca62c9edc42339d01c2a3..54a6056e138abe24f0fb91dc8d92c391f8d57c01 100644
--- a/reviewboard/attachments/models.py
+++ b/reviewboard/attachments/models.py
@@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 from django.db.models import Max
 from django.utils.translation import gettext_lazy as _
-from djblets.db.fields import RelationCounterField
+from djblets.db.fields import JSONField, RelationCounterField
 
 from reviewboard.admin.server import build_server_url
 from reviewboard.attachments.managers import FileAttachmentManager
@@ -93,6 +93,8 @@ class FileAttachment(models.Model):
                                                    '%Y', '%m', '%d'))
     mimetype = models.CharField(_('mimetype'), max_length=256, blank=True)
 
+    extra_data = JSONField(null=True)
+
     # repo_path, repo_revision, and repository are used to identify
     # FileAttachments associated with committed binary files in a source tree.
     # They are not used for new files that don't yet have a revision.
diff --git a/reviewboard/attachments/tests.py b/reviewboard/attachments/tests.py
index 0c84740de4db2d5a2c775372a1ce49c2f26f06ae..7a598ad5ef047a8f82aa0ec34999d5e54d6090e9 100644
--- a/reviewboard/attachments/tests.py
+++ b/reviewboard/attachments/tests.py
@@ -1,5 +1,7 @@
+import json
 import mimeparse
 import os
+from datetime import datetime
 
 from django.conf import settings
 from django.contrib.auth.models import AnonymousUser, User
@@ -82,9 +84,12 @@ class FileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertTrue(form.is_valid())
 
         file_attachment = form.create()
+        file_attachment.refresh_from_db()
+
         self.assertTrue(os.path.basename(file_attachment.file.name).endswith(
             '__logo.png'))
         self.assertEqual(file_attachment.mimetype, 'image/png')
+        self.assertEqual(file_attachment.extra_data, {})
 
     @add_fixtures(['test_users', 'test_scmtools'])
     def test_upload_file_with_history(self):
@@ -185,6 +190,164 @@ class FileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertEqual(file_attachment.attachment_history.display_position,
                          1)
 
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_upload_file_with_extra_data(self):
+        """Testing uploading a file attachment with extra data"""
+        class TestObject():
+            def to_json(self):
+                return {
+                    'foo': 'bar'
+                }
+
+        review_request = self.create_review_request(publish=True)
+
+        file = self.make_uploaded_file()
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': {
+                    'test_bool': True,
+                    'test_date': datetime(2023, 1, 26, 5, 30, 3, 123456),
+                    'test_int': 1,
+                    'test_list': [1, 2, 3],
+                    'test_nested_dict': {
+                        'foo': 2,
+                        'bar': 'baz',
+                    },
+                    'test_none': None,
+                    'test_obj': TestObject(),
+                    'test_str': 'test',
+                }
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {
+            'test_bool': True,
+            'test_date': '2023-01-26T05:30:03.123',
+            'test_int': 1,
+            'test_list': [1, 2, 3],
+            'test_nested_dict': {
+                'foo': 2,
+                'bar': 'baz',
+            },
+            'test_none': None,
+            'test_obj': {
+                'foo': 'bar',
+            },
+            'test_str': 'test',
+        })
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_upload_file_with_extra_data_string(self):
+        """Testing uploading a file attachment with extra data passed as a
+        JSON string
+        """
+        review_request = self.create_review_request(publish=True)
+
+        file = self.make_uploaded_file()
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': json.dumps({
+                    'test_bool': True,
+                    'test_int': 1,
+                    'test_list': [1, 2, 3],
+                    'test_nested_dict': {
+                        'foo': 2,
+                        'bar': 'baz',
+                    },
+                    'test_none': None,
+                    'test_str': 'test',
+                })
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {
+            'test_bool': True,
+            'test_int': 1,
+            'test_list': [1, 2, 3],
+            'test_nested_dict': {
+                'foo': 2,
+                'bar': 'baz',
+            },
+            'test_none': None,
+            'test_str': 'test',
+        })
+
+    @add_fixtures(['test_users', 'test_scmtools'])
+    def test_upload_file_with_extra_data_empties(self):
+        """Testing uploading a file attachment with extra data that contains
+        empty values
+        """
+        review_request = self.create_review_request(publish=True)
+        file = self.make_uploaded_file()
+
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': {}
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': json.dumps(None)
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': None
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadFileForm(
+            review_request,
+            data={
+                'extra_data': {
+                    'test_list': [],
+                    'test_nested_dict': {},
+                    'test_none': None,
+                    'test_str': '',
+                }
+            },
+            files={'path': file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create()
+        file_attachment.refresh_from_db()
+        self.assertEqual(file_attachment.extra_data, {
+            'test_list': [],
+            'test_nested_dict': {},
+            'test_none': None,
+            'test_str': '',
+        })
+
     def test_is_from_diff_with_no_association(self):
         """Testing FileAttachment.is_from_diff with standard attachment"""
         file_attachment = FileAttachment()
@@ -260,8 +423,11 @@ class UserFileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertTrue(form.is_valid())
 
         file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
         self.assertFalse(file_attachment.file)
         self.assertEqual(file_attachment.user, user)
+        self.assertEqual(file_attachment.extra_data, {})
 
         uploaded_file = self.make_uploaded_file()
         form = UploadUserFileForm(files={
@@ -270,6 +436,7 @@ class UserFileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertTrue(form.is_valid())
 
         file_attachment = form.update(file_attachment)
+        file_attachment.refresh_from_db()
 
         self.assertTrue(os.path.basename(file_attachment.file.name).endswith(
             '__logo.png'))
@@ -291,6 +458,164 @@ class UserFileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertTrue(os.path.basename(file_attachment.file.name).endswith(
             '__logo.png'))
         self.assertEqual(file_attachment.mimetype, 'image/png')
+        self.assertEqual(file_attachment.extra_data, {})
+
+    def test_user_file_with_extra_data(self):
+        """Testing user FileAttachment create with extra data"""
+        class TestObject():
+            def to_json(self):
+                return {
+                    'foo': 'bar'
+                }
+
+        user = User.objects.get(username='doc')
+        uploaded_file = self.make_uploaded_file()
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': {
+                    'test_bool': True,
+                    'test_date': datetime(2023, 1, 26, 5, 30, 3, 123456),
+                    'test_int': 1,
+                    'test_list': [1, 2, 3],
+                    'test_nested_dict': {
+                        'foo': 2,
+                        'bar': 'baz',
+                    },
+                    'test_none': None,
+                    'test_obj': TestObject(),
+                    'test_str': 'test',
+                }
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.user, user)
+        self.assertTrue(os.path.basename(file_attachment.file.name).endswith(
+            '__logo.png'))
+        self.assertEqual(file_attachment.mimetype, 'image/png')
+        self.assertEqual(file_attachment.extra_data, {
+            'test_bool': True,
+            'test_date': '2023-01-26T05:30:03.123',
+            'test_int': 1,
+            'test_list': [1, 2, 3],
+            'test_nested_dict': {
+                'foo': 2,
+                'bar': 'baz',
+            },
+            'test_none': None,
+            'test_obj': {
+                'foo': 'bar',
+            },
+            'test_str': 'test',
+        })
+
+    def test_user_file_with_extra_data_string(self):
+        """Testing user FileAttachment create with extra data passed as a
+        JSON string
+        """
+        user = User.objects.get(username='doc')
+        uploaded_file = self.make_uploaded_file()
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': json.dumps({
+                    'test_bool': True,
+                    'test_int': 1,
+                    'test_list': [1, 2, 3],
+                    'test_nested_dict': {
+                        'foo': 2,
+                        'bar': 'baz',
+                    },
+                    'test_none': None,
+                    'test_str': 'test',
+                })
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {
+            'test_bool': True,
+            'test_int': 1,
+            'test_list': [1, 2, 3],
+            'test_nested_dict': {
+                'foo': 2,
+                'bar': 'baz',
+            },
+            'test_none': None,
+            'test_str': 'test',
+        })
+
+    def test_user_file_with_extra_data_empties(self):
+        """Testing user FileAttachment create with extra data that contains
+        empty values
+        """
+        user = User.objects.get(username='doc')
+        uploaded_file = self.make_uploaded_file()
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': {}
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': json.dumps(None)
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': None
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': {
+                    'test_list': [],
+                    'test_nested_dict': {},
+                    'test_none': None,
+                    'test_str': '',
+                }
+            },
+            files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {
+            'test_list': [],
+            'test_nested_dict': {},
+            'test_none': None,
+            'test_str': '',
+        })
 
     @add_fixtures(['test_site'])
     def test_user_file_local_sites(self):
@@ -306,6 +631,63 @@ class UserFileAttachmentTests(BaseFileAttachmentTestCase):
         self.assertEqual(file_attachment.user, user)
         self.assertEqual(file_attachment.local_site, local_site)
 
+    def test_user_file_update_with_extra_data(self):
+        """Testing user FileAttachment update with extra data"""
+        class TestObject():
+            def to_json(self):
+                return {
+                    'foo': 'bar'
+                }
+
+        user = User.objects.get(username='doc')
+        uploaded_file = self.make_uploaded_file()
+
+        form = UploadUserFileForm(files={'path': uploaded_file})
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.create(user)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {})
+
+        form = UploadUserFileForm(
+            data={
+                'extra_data': {
+                    'test_bool': True,
+                    'test_date': datetime(2023, 1, 26, 5, 30, 3, 123456),
+                    'test_int': 1,
+                    'test_list': [1, 2, 3],
+                    'test_nested_dict': {
+                        'foo': 2,
+                        'bar': 'baz',
+                    },
+                    'test_none': None,
+                    'test_obj': TestObject(),
+                    'test_str': 'test',
+                }
+            }
+        )
+        self.assertTrue(form.is_valid())
+
+        file_attachment = form.update(file_attachment)
+        file_attachment.refresh_from_db()
+
+        self.assertEqual(file_attachment.extra_data, {
+            'test_bool': True,
+            'test_date': '2023-01-26T05:30:03.123',
+            'test_int': 1,
+            'test_list': [1, 2, 3],
+            'test_nested_dict': {
+                'foo': 2,
+                'bar': 'baz',
+            },
+            'test_none': None,
+            'test_obj': {
+                'foo': 'bar',
+            },
+            'test_str': 'test',
+        })
+
     @add_fixtures(['test_site'])
     def test_user_file_is_accessible_by(self):
         """Testing user FileAttachment.is_accessible_by"""
diff --git a/reviewboard/webapi/resources/base_file_attachment.py b/reviewboard/webapi/resources/base_file_attachment.py
index 3749760af32b715bbcc927a115fdc5961cd7e963..bc6501723943c5890301c452da0955f143547fe6 100644
--- a/reviewboard/webapi/resources/base_file_attachment.py
+++ b/reviewboard/webapi/resources/base_file_attachment.py
@@ -1,4 +1,4 @@
-from djblets.webapi.fields import IntFieldType, StringFieldType
+from djblets.webapi.fields import IntFieldType, DictFieldType, StringFieldType
 
 from reviewboard.attachments.models import FileAttachment
 from reviewboard.webapi.base import WebAPIResource
@@ -11,24 +11,26 @@ class BaseFileAttachmentResource(WebAPIResource):
     model = FileAttachment
     name = 'file_attachment'
     fields = {
-        'id': {
-            'type': IntFieldType,
-            'description': 'The numeric ID of the file.',
+        'absolute_url': {
+            'type': StringFieldType,
+            'description': 'The absolute URL of the file, for downloading '
+                           'purposes.',
+            'added_in': '2.0',
         },
         'caption': {
             'type': StringFieldType,
             'description': "The file's descriptive caption.",
         },
+        'extra_data': {
+            'type': DictFieldType,
+            'description': 'Extra data as part of the file attachment. '
+                           'This can be set by the API or extensions.',
+            'added_in': '6.0',
+        },
         'filename': {
             'type': StringFieldType,
             'description': "The name of the file.",
         },
-        'absolute_url': {
-            'type': StringFieldType,
-            'description': "The absolute URL of the file, for downloading "
-                           "purposes.",
-            'added_in': '2.0',
-        },
         'icon_url': {
             'type': StringFieldType,
             'description': 'The URL to a 24x24 icon representing this file. '
@@ -36,6 +38,10 @@ class BaseFileAttachmentResource(WebAPIResource):
                            'property will be removed in a future version.',
             'deprecated_in': '2.5',
         },
+        'id': {
+            'type': IntFieldType,
+            'description': 'The numeric ID of the file.',
+        },
         'mimetype': {
             'type': StringFieldType,
             'description': 'The mimetype for the file.',
diff --git a/reviewboard/webapi/resources/base_review_request_file_attachment.py b/reviewboard/webapi/resources/base_review_request_file_attachment.py
index 2e5de31e0ea54f1736c14719cfd2ab3318ba9238..3ce27d7629febb6f3091f585b88c1747fd040de5 100644
--- a/reviewboard/webapi/resources/base_review_request_file_attachment.py
+++ b/reviewboard/webapi/resources/base_review_request_file_attachment.py
@@ -1,17 +1,25 @@
+from __future__ import annotations
+
 import logging
+from typing import Any, Dict, Optional, Union
 
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db.models import Q
+from django.http import HttpRequest
 from djblets.webapi.decorators import (webapi_login_required,
                                        webapi_response_errors,
                                        webapi_request_fields)
-from djblets.webapi.errors import (DOES_NOT_EXIST, INVALID_FORM_DATA,
-                                   NOT_LOGGED_IN, PERMISSION_DENIED)
+from djblets.webapi.errors import (DOES_NOT_EXIST,
+                                   INVALID_FORM_DATA,
+                                   NOT_LOGGED_IN,
+                                   PERMISSION_DENIED,
+                                   WebAPIError)
 from djblets.webapi.fields import FileFieldType, IntFieldType, StringFieldType
 
 from reviewboard.attachments.forms import UploadFileForm
 from reviewboard.attachments.models import FileAttachment
 from reviewboard.site.urlresolvers import local_site_reverse
+from reviewboard.webapi.base import ImportExtraDataError
 from reviewboard.webapi.decorators import webapi_check_local_site
 from reviewboard.webapi.resources import resources
 from reviewboard.webapi.resources.base_file_attachment import \
@@ -133,8 +141,15 @@ class BaseReviewRequestFileAttachmentResource(BaseFileAttachmentResource):
                 'added_in': '2.5',
             },
         },
+        allow_unknown=True
     )
-    def create(self, request, *args, **kwargs):
+    def create(
+        self,
+        request: HttpRequest,
+        extra_fields: Dict[str, Any] = {},
+        *args,
+        **kwargs,
+    ) -> Union[tuple, WebAPIError]:
         """Creates a new file from a file attachment.
 
         This accepts any file type and associates it with a draft of a
@@ -150,6 +165,9 @@ class BaseReviewRequestFileAttachmentResource(BaseFileAttachmentResource):
 
             <Content here>
             -- SoMe BoUnDaRy --
+
+        Extra data can be stored for later lookup. See
+        :ref:`webapi2.0-extra-data` for more information.
         """
         try:
             review_request = \
@@ -177,6 +195,14 @@ class BaseReviewRequestFileAttachmentResource(BaseFileAttachmentResource):
                 },
             }
 
+        if extra_fields:
+            try:
+                self.import_extra_data(file, file.extra_data, extra_fields)
+            except ImportExtraDataError as e:
+                return e.error_payload
+
+            file.save(update_fields=('extra_data',))
+
         return 201, {
             self.item_result_key: self.serialize_object(
                 file, request=request, *args, **kwargs),
@@ -196,13 +222,23 @@ class BaseReviewRequestFileAttachmentResource(BaseFileAttachmentResource):
                 'description': 'The thumbnail data for the file.',
                 'added_in': '1.7.7',
             },
-        }
+        },
+        allow_unknown=True
     )
-    def update(self, request, caption=None, thumbnail=None, *args, **kwargs):
+    def update(
+        self,
+        request: HttpRequest,
+        caption: Optional[str] = None,
+        thumbnail: Optional[bytes] = None,
+        extra_fields: Dict[str, Any] = {},
+        *args,
+        **kwargs,
+    ) -> Union[tuple, WebAPIError]:
         """Updates the file's data.
 
-        This allows updating the file in a draft. Currently, only the caption
-        and the thumbnail can be updated.
+        This allows updating the file in a draft. Currently, only the caption,
+        thumbnail and extra_data can be updated. See
+        :ref:`webapi2.0-extra-data` for more information.
         """
         try:
             review_request = \
@@ -219,15 +255,22 @@ class BaseReviewRequestFileAttachmentResource(BaseFileAttachmentResource):
         except ObjectDoesNotExist:
             return DOES_NOT_EXIST
 
-        if caption is not None:
-            try:
-                resources.review_request_draft.prepare_draft(request,
-                                                             review_request)
-            except PermissionDenied:
-                return self.get_no_access_error(request)
+        try:
+            resources.review_request_draft.prepare_draft(
+                request,
+                review_request)
+        except PermissionDenied:
+            return self.get_no_access_error(request)
 
+        if caption is not None:
             file.draft_caption = caption
-            file.save()
+
+        try:
+            self.import_extra_data(file, file.extra_data, extra_fields)
+        except ImportExtraDataError as e:
+            return e.error_payload
+
+        file.save()
 
         if thumbnail is not None:
             try:
diff --git a/reviewboard/webapi/resources/user_file_attachment.py b/reviewboard/webapi/resources/user_file_attachment.py
index b47151dafa6936872a11571a4353d029b832b060..c5020cc4c6782c20ad60442a5b8f55707a9112fa 100644
--- a/reviewboard/webapi/resources/user_file_attachment.py
+++ b/reviewboard/webapi/resources/user_file_attachment.py
@@ -1,16 +1,25 @@
+from __future__ import annotations
+
+from typing import Any, Dict, Optional, Union
+
 from django.core.exceptions import ObjectDoesNotExist
+from django.http import HttpRequest
 from djblets.util.decorators import augment_method_from
 from djblets.webapi.decorators import (webapi_login_required,
                                        webapi_request_fields,
                                        webapi_response_errors)
-from djblets.webapi.errors import (DOES_NOT_EXIST, DUPLICATE_ITEM,
-                                   INVALID_FORM_DATA, NOT_LOGGED_IN,
-                                   PERMISSION_DENIED)
+from djblets.webapi.errors import (DOES_NOT_EXIST,
+                                   DUPLICATE_ITEM,
+                                   INVALID_FORM_DATA,
+                                   NOT_LOGGED_IN,
+                                   PERMISSION_DENIED,
+                                   WebAPIError)
 from djblets.webapi.fields import FileFieldType, StringFieldType
 
 from reviewboard.admin.server import build_server_url
 from reviewboard.attachments.forms import UploadUserFileForm
 from reviewboard.site.urlresolvers import local_site_reverse
+from reviewboard.webapi.base import ImportExtraDataError
 from reviewboard.webapi.decorators import webapi_check_local_site
 from reviewboard.webapi.resources import resources
 from reviewboard.webapi.resources.base_file_attachment import \
@@ -112,8 +121,16 @@ class UserFileAttachmentResource(BaseFileAttachmentResource):
                 'description': 'The file to upload.',
             },
         },
+        allow_unknown=True
     )
-    def create(self, request, local_site_name=None, *args, **kwargs):
+    def create(
+        self,
+        request: HttpRequest,
+        local_site_name: Optional[str] = None,
+        extra_fields: Dict[str, Any] = {},
+        *args,
+        **kwargs,
+    ) -> Union[tuple, WebAPIError]:
         """Creates a new file attachment that is owned by the user.
 
         This accepts any file type and associates it with the user. Optionally,
@@ -130,6 +147,9 @@ class UserFileAttachmentResource(BaseFileAttachmentResource):
 
             <Content here>
             -- SoMe BoUnDaRy --
+
+        Extra data can be stored for later lookup. See
+        :ref:`webapi2.0-extra-data` for more information.
         """
         try:
             user = resources.user.get_object(
@@ -152,6 +172,16 @@ class UserFileAttachmentResource(BaseFileAttachmentResource):
 
         file_attachment = form.create(request.user, local_site)
 
+        if extra_fields:
+            try:
+                self.import_extra_data(file_attachment,
+                                       file_attachment.extra_data,
+                                       extra_fields)
+            except ImportExtraDataError as e:
+                return e.error_payload
+
+            file_attachment.save(update_fields=('extra_data',))
+
         return 201, {
             self.item_result_key: self.serialize_object(
                 file_attachment, request=request, *args, **kwargs),
@@ -174,8 +204,16 @@ class UserFileAttachmentResource(BaseFileAttachmentResource):
                 'description': 'The file to upload.',
             },
         },
+        allow_unknown=True,
     )
-    def update(self, request, local_site_name=None, *args, **kwargs):
+    def update(
+        self,
+        request: HttpRequest,
+        local_site_name: Optional[str] = None,
+        extra_fields: Dict[str, Any] = {},
+        *args,
+        **kwargs,
+    ) -> Union[tuple, WebAPIError]:
         """Updates the file attachment's data.
 
         This allows updating information on the file attachment. It also allows
@@ -222,6 +260,15 @@ class UserFileAttachmentResource(BaseFileAttachmentResource):
 
         file_attachment = form.update(file_attachment)
 
+        try:
+            self.import_extra_data(file_attachment,
+                                   file_attachment.extra_data,
+                                   extra_fields)
+        except ImportExtraDataError as e:
+            return e.error_payload
+
+        file_attachment.save(update_fields=('extra_data',))
+
         return 200, {
             self.item_result_key: file_attachment
         }
diff --git a/reviewboard/webapi/tests/test_file_attachment.py b/reviewboard/webapi/tests/test_file_attachment.py
index 3f9d4ea4c358395c693fee35b632fd5dd09576aa..b751b9029d7bb39ffe64eef25d4cd8b6bd6c7e5e 100644
--- a/reviewboard/webapi/tests/test_file_attachment.py
+++ b/reviewboard/webapi/tests/test_file_attachment.py
@@ -9,11 +9,15 @@ from reviewboard.webapi.tests.mimetypes import (file_attachment_item_mimetype,
 from reviewboard.webapi.tests.mixins import (BasicTestsMetaclass,
                                              ReviewRequestChildItemMixin,
                                              ReviewRequestChildListMixin)
+from reviewboard.webapi.tests.mixins_extra_data import (ExtraDataItemMixin,
+                                                        ExtraDataListMixin)
 from reviewboard.webapi.tests.urls import (get_file_attachment_item_url,
                                            get_file_attachment_list_url)
 
 
-class ResourceListTests(ReviewRequestChildListMixin, BaseWebAPITestCase,
+class ResourceListTests(ReviewRequestChildListMixin,
+                        BaseWebAPITestCase,
+                        ExtraDataListMixin,
                         metaclass=BasicTestsMetaclass):
     """Testing the FileAttachmentResource list APIs."""
     fixtures = ['test_users']
@@ -27,6 +31,7 @@ class ResourceListTests(ReviewRequestChildListMixin, BaseWebAPITestCase,
 
     def compare_item(self, item_rsp, attachment):
         self.assertEqual(item_rsp['id'], attachment.pk)
+        self.assertEqual(item_rsp['extra_data'], attachment.extra_data)
         self.assertEqual(item_rsp['filename'], attachment.filename)
         self.assertEqual(item_rsp['revision'], attachment.attachment_revision)
 
@@ -200,7 +205,9 @@ class ResourceListTests(ReviewRequestChildListMixin, BaseWebAPITestCase,
             self.assertEqual(history.latest_revision, 0)
 
 
-class ResourceItemTests(ReviewRequestChildItemMixin, BaseWebAPITestCase,
+class ResourceItemTests(ReviewRequestChildItemMixin,
+                        BaseWebAPITestCase,
+                        ExtraDataItemMixin,
                         metaclass=BasicTestsMetaclass):
     """Testing the FileAttachmentResource item APIs."""
     fixtures = ['test_users']
@@ -215,6 +222,7 @@ class ResourceItemTests(ReviewRequestChildItemMixin, BaseWebAPITestCase,
 
     def compare_item(self, item_rsp, attachment):
         self.assertEqual(item_rsp['id'], attachment.pk)
+        self.assertEqual(item_rsp['extra_data'], attachment.extra_data)
         self.assertEqual(item_rsp['filename'], attachment.filename)
         self.assertEqual(item_rsp['revision'], attachment.attachment_revision)
         self.assertEqual(item_rsp['absolute_url'],
diff --git a/reviewboard/webapi/tests/test_user_file_attachment.py b/reviewboard/webapi/tests/test_user_file_attachment.py
index 5257eb24a98aaf62e8324ba8d5cbd66b9e9ff4d7..9b9e1270b066cf0a2611b0341264f3e26dab107d 100644
--- a/reviewboard/webapi/tests/test_user_file_attachment.py
+++ b/reviewboard/webapi/tests/test_user_file_attachment.py
@@ -8,11 +8,15 @@ from reviewboard.webapi.tests.mimetypes import (
     user_file_attachment_item_mimetype,
     user_file_attachment_list_mimetype)
 from reviewboard.webapi.tests.mixins import BasicTestsMetaclass
+from reviewboard.webapi.tests.mixins_extra_data import (ExtraDataItemMixin,
+                                                        ExtraDataListMixin)
 from reviewboard.webapi.tests.urls import (get_user_file_attachment_item_url,
                                            get_user_file_attachment_list_url)
 
 
-class ResourceListTests(BaseWebAPITestCase, metaclass=BasicTestsMetaclass):
+class ResourceListTests(BaseWebAPITestCase,
+                        ExtraDataListMixin,
+                        metaclass=BasicTestsMetaclass):
     """Testing the UserFileAttachmentResource list APIs."""
 
     fixtures = ['test_users', 'test_site']
@@ -22,6 +26,7 @@ class ResourceListTests(BaseWebAPITestCase, metaclass=BasicTestsMetaclass):
     def compare_item(self, item_rsp, attachment):
         self.assertEqual(item_rsp['id'], attachment.pk)
         self.assertEqual(item_rsp['filename'], attachment.filename)
+        self.assertEqual(item_rsp['extra_data'], attachment.extra_data)
 
     #
     # HTTP GET tests
@@ -103,7 +108,9 @@ class ResourceListTests(BaseWebAPITestCase, metaclass=BasicTestsMetaclass):
         self.check_post_result(None, rsp, caption)
 
 
-class ResourceItemTests(BaseWebAPITestCase, metaclass=BasicTestsMetaclass):
+class ResourceItemTests(BaseWebAPITestCase,
+                        ExtraDataItemMixin,
+                        metaclass=BasicTestsMetaclass):
     """Testing the UserFileAttachmentResource item APIs."""
 
     fixtures = ['test_users']
@@ -113,6 +120,7 @@ class ResourceItemTests(BaseWebAPITestCase, metaclass=BasicTestsMetaclass):
     def compare_item(self, item_rsp, attachment):
         self.assertEqual(item_rsp['id'], attachment.pk)
         self.assertEqual(item_rsp['filename'], attachment.filename)
+        self.assertEqual(item_rsp['extra_data'], attachment.extra_data)
 
     #
     # HTTP DELETE tests
