diff --git a/djblets/webapi/decorators.py b/djblets/webapi/decorators.py
index 30b4907173154c3885a5c9858aa9af0a6cf76d17..f044060ffa954aad6687b5ea5827ff98a1df1e18 100644
--- a/djblets/webapi/decorators.py
+++ b/djblets/webapi/decorators.py
@@ -1,8 +1,11 @@
 """Decorators used for WebAPI views."""
 
+from __future__ import annotations
+
 import inspect
 import logging
 from functools import update_wrapper, wraps
+from typing import Callable, TYPE_CHECKING
 
 from django.core.exceptions import ValidationError
 from django.http import HttpRequest
@@ -15,6 +18,9 @@ from djblets.webapi.fields import (BooleanFieldType,
                                    IntFieldType,
                                    StringFieldType)
 
+if TYPE_CHECKING:
+    from djblets.webapi.resources.base import WebAPIResourceHandlerResult
+
 
 logger = logging.getLogger(__name__)
 
@@ -126,6 +132,7 @@ def copy_webapi_decorator_data(from_func, to_func):
     from_optional_fields = getattr(from_func, 'optional_fields', {}).copy()
     to_required_fields = getattr(to_func, 'required_fields', {}).copy()
     to_optional_fields = getattr(to_func, 'optional_fields', {}).copy()
+    webapi_docs = getattr(to_func, 'webapi_docs', None)
 
     update_wrapper(to_func, from_func)
 
@@ -139,6 +146,9 @@ def copy_webapi_decorator_data(from_func, to_func):
         to_func.optional_fields = from_optional_fields
         to_func.optional_fields.update(to_optional_fields)
 
+    if webapi_docs is not None:
+        to_func.webapi_docs = webapi_docs
+
     return to_func
 
 
@@ -353,3 +363,30 @@ def webapi_request_fields(required={}, optional={}, allow_unknown=False):
         return _validate
 
     return _dec
+
+
+def webapi_docs(
+    docs: str,
+) -> Callable[..., Callable[..., WebAPIResourceHandlerResult]]:
+    """Add documentation for an API method.
+
+    Args:
+        docs (str):
+            The documentation to attach to the method.
+
+    Returns:
+        callable:
+        The decorated function.
+    """
+    @webapi_decorator
+    def _dec(
+        view_func: Callable[..., WebAPIResourceHandlerResult],
+    ) -> Callable[..., WebAPIResourceHandlerResult]:
+        def _inner(*args, **kwargs) -> WebAPIResourceHandlerResult:
+            return view_func(*args, **kwargs)
+
+        _inner.webapi_docs = docs
+
+        return _inner
+
+    return _dec
diff --git a/djblets/webapi/tests/test_decorators.py b/djblets/webapi/tests/test_decorators.py
index a572ecaf7a6100b9d9a87a811522567a9f29056f..de593826b33475156d391cc867bef4837b4cde5e 100644
--- a/djblets/webapi/tests/test_decorators.py
+++ b/djblets/webapi/tests/test_decorators.py
@@ -1,5 +1,7 @@
 """Unit tests for WebAPI decorators."""
 
+from __future__ import annotations
+
 from django.contrib.auth.models import AnonymousUser, User
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.test.client import RequestFactory
@@ -7,6 +9,7 @@ from django.test.client import RequestFactory
 from djblets.testing.testcases import TestCase
 from djblets.util.decorators import augment_method_from
 from djblets.webapi.decorators import (copy_webapi_decorator_data,
+                                       webapi_docs,
                                        webapi_login_required,
                                        webapi_permission_required,
                                        webapi_request_fields,
@@ -23,12 +26,12 @@ from djblets.webapi.fields import (BooleanFieldType,
 class WebAPIDecoratorTests(TestCase):
     """Unit tests for djblets.webapi.decorators."""
 
-    def test_copy_webapi_decorator_data(self):
+    def test_copy_webapi_decorator_data(self) -> None:
         """Testing copy_webapi_decorator_data"""
         def func1() -> None:
             """Function 1"""
 
-        def func2():
+        def func2() -> None:
             """Function 2"""
 
         func1.test1 = True
@@ -46,7 +49,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(func2.test2)
         self.assertEqual(func2.response_errors, set(['a', 'b', 'c', 'd']))
         self.assertEqual(func2.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func2.__doc__, 'Function 1')
         self.assertEqual(func2.__name__, 'func1')
@@ -58,7 +61,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(func1, 'test2'))
         self.assertEqual(func1.response_errors, set(['a', 'b']))
 
-    def test_webapi_response_errors_state(self):
+    def test_webapi_response_errors_state(self) -> None:
         """Testing @webapi_response_errors state"""
         def orig_func() -> None:
             """Function 1"""
@@ -68,7 +71,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(orig_func, 'response_errors'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -78,9 +81,9 @@ class WebAPIDecoratorTests(TestCase):
             '<locals>.orig_func')
         self.assertTrue(hasattr(func, 'response_errors'))
         self.assertEqual(func.response_errors,
-                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+                         {DOES_NOT_EXIST, NOT_LOGGED_IN})
 
-    def test_webapi_response_errors_preserves_state(self):
+    def test_webapi_response_errors_preserves_state(self) -> None:
         """Testing @webapi_response_errors preserves decorator state"""
         @webapi_response_errors(DOES_NOT_EXIST)
         @webapi_response_errors(NOT_LOGGED_IN)
@@ -88,7 +91,7 @@ class WebAPIDecoratorTests(TestCase):
             """Function 1"""
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'func')
@@ -98,9 +101,9 @@ class WebAPIDecoratorTests(TestCase):
             '<locals>.func')
         self.assertTrue(hasattr(func, 'response_errors'))
         self.assertEqual(func.response_errors,
-                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+                         {DOES_NOT_EXIST, NOT_LOGGED_IN})
 
-    def test_webapi_response_errors_call(self):
+    def test_webapi_response_errors_call(self) -> None:
         """Testing @webapi_response_errors calls original function"""
         @webapi_response_errors(DOES_NOT_EXIST, NOT_LOGGED_IN)
         def func():
@@ -110,7 +113,7 @@ class WebAPIDecoratorTests(TestCase):
 
         self.assertTrue(hasattr(func, 'seen'))
 
-    def test_webapi_login_required_state(self):
+    def test_webapi_login_required_state(self) -> None:
         """Testing @webapi_login_required state"""
         def orig_func() -> None:
             """Function 1"""
@@ -121,7 +124,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(orig_func, 'response_errors'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -132,9 +135,9 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(func, 'response_errors'))
         self.assertTrue(hasattr(func, 'login_required'))
         self.assertTrue(func.login_required)
-        self.assertEqual(func.response_errors, set([NOT_LOGGED_IN]))
+        self.assertEqual(func.response_errors, {NOT_LOGGED_IN})
 
-    def test_webapi_login_required_preserves_state(self):
+    def test_webapi_login_required_preserves_state(self) -> None:
         """Testing @webapi_login_required preserves decorator state"""
         @webapi_response_errors(DOES_NOT_EXIST)
         def orig_func() -> None:
@@ -145,7 +148,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(orig_func, 'login_required'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -157,9 +160,9 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(func, 'login_required'))
         self.assertTrue(func.login_required)
         self.assertEqual(func.response_errors,
-                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+                         {DOES_NOT_EXIST, NOT_LOGGED_IN})
 
-    def test_webapi_login_required_call_when_authenticated(self):
+    def test_webapi_login_required_call_when_authenticated(self) -> None:
         """Testing @webapi_login_required calls when authenticated"""
         @webapi_login_required
         def func(request):
@@ -172,7 +175,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(func, 'seen'))
         self.assertEqual(result, None)
 
-    def test_webapi_login_required_call_when_anonymous(self):
+    def test_webapi_login_required_call_when_anonymous(self) -> None:
         """Testing @webapi_login_required calls when anonymous"""
         @webapi_login_required
         def func(request):
@@ -185,7 +188,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(func, 'seen'))
         self.assertEqual(result, NOT_LOGGED_IN)
 
-    def test_webapi_permission_required_state(self):
+    def test_webapi_permission_required_state(self) -> None:
         """Testing @webapi_permission_required state"""
         def orig_func() -> None:
             """Function 1"""
@@ -195,7 +198,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(orig_func, 'response_errors'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -205,9 +208,9 @@ class WebAPIDecoratorTests(TestCase):
             '<locals>.orig_func')
         self.assertTrue(hasattr(func, 'response_errors'))
         self.assertEqual(func.response_errors,
-                         set([NOT_LOGGED_IN, PERMISSION_DENIED]))
+                         {NOT_LOGGED_IN, PERMISSION_DENIED})
 
-    def test_webapi_permission_required_preserves_state(self):
+    def test_webapi_permission_required_preserves_state(self) -> None:
         """Testing @webapi_permission_required preserves decorator state"""
         @webapi_response_errors(DOES_NOT_EXIST)
         def orig_func() -> None:
@@ -216,7 +219,7 @@ class WebAPIDecoratorTests(TestCase):
         func = webapi_permission_required('myperm')(orig_func)
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -227,10 +230,9 @@ class WebAPIDecoratorTests(TestCase):
             'orig_func')
         self.assertTrue(hasattr(func, 'response_errors'))
         self.assertEqual(func.response_errors,
-                         set([DOES_NOT_EXIST, NOT_LOGGED_IN,
-                              PERMISSION_DENIED]))
+                         {DOES_NOT_EXIST, NOT_LOGGED_IN, PERMISSION_DENIED})
 
-    def test_webapi_permission_required_call_when_anonymous(self):
+    def test_webapi_permission_required_call_when_anonymous(self) -> None:
         """Testing @webapi_permission_required calls when anonymous"""
         @webapi_permission_required('foo')
         def func(request):
@@ -243,7 +245,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(func, 'seen'))
         self.assertEqual(result, NOT_LOGGED_IN)
 
-    def test_webapi_permission_required_call_when_has_permission(self):
+    def test_webapi_permission_required_call_when_has_permission(self) -> None:
         """Testing @webapi_permission_required calls when has permission"""
         @webapi_permission_required('foo')
         def func(request):
@@ -257,7 +259,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(func, 'seen'))
         self.assertEqual(result, None)
 
-    def test_webapi_permission_required_call_when_no_permission(self):
+    def test_webapi_permission_required_call_when_no_permission(self) -> None:
         """Testing @webapi_permission_required calls when no permission"""
         @webapi_permission_required('foo')
         def func(request):
@@ -271,7 +273,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(func, 'seen'))
         self.assertEqual(result, PERMISSION_DENIED)
 
-    def test_webapi_request_fields_state(self):
+    def test_webapi_request_fields_state(self) -> None:
         """Testing @webapi_request_fields state"""
         def orig_func() -> None:
             """Function 1"""
@@ -279,14 +281,14 @@ class WebAPIDecoratorTests(TestCase):
         required = {
             'required_param': {
                 'type': BooleanFieldType,
-                'description': 'Required param'
+                'description': 'Required param',
             },
         }
 
         optional = {
             'optional_param': {
                 'type': BooleanFieldType,
-                'description': 'Optional param'
+                'description': 'Optional param',
             },
         }
 
@@ -297,7 +299,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertFalse(hasattr(orig_func, 'response_errors'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__doc__, 'Function 1')
         self.assertEqual(func.__name__, 'orig_func')
@@ -312,19 +314,19 @@ class WebAPIDecoratorTests(TestCase):
         self.assertEqual(func.optional_fields, optional)
         self.assertEqual(func.response_errors, set([INVALID_FORM_DATA]))
 
-    def test_webapi_request_fields_preserves_state(self):
+    def test_webapi_request_fields_preserves_state(self) -> None:
         """Testing @webapi_request_fields preserves decorator state"""
         required1 = {
             'required1': {
                 'type': BooleanFieldType,
-                'description': 'Required param'
+                'description': 'Required param',
             },
         }
 
         optional1 = {
             'optional1': {
                 'type': BooleanFieldType,
-                'description': 'Optional param'
+                'description': 'Optional param',
             },
         }
 
@@ -336,14 +338,14 @@ class WebAPIDecoratorTests(TestCase):
         required2 = {
             'required2': {
                 'type': BooleanFieldType,
-                'description': 'Required param'
+                'description': 'Required param',
             },
         }
 
         optional2 = {
             'optional2': {
                 'type': BooleanFieldType,
-                'description': 'Optional param'
+                'description': 'Optional param',
             },
         }
 
@@ -359,7 +361,7 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(orig_func, 'response_errors'))
 
         self.assertEqual(func.__annotations__, {
-            'return': None,
+            'return': 'None',
         })
         self.assertEqual(func.__name__, 'orig_func')
         self.assertEqual(func.__doc__, 'Function 1')
@@ -373,18 +375,18 @@ class WebAPIDecoratorTests(TestCase):
         self.assertEqual(func.required_fields, expected_required)
         self.assertEqual(func.optional_fields, expected_optional)
         self.assertEqual(func.response_errors,
-                         set([DOES_NOT_EXIST, INVALID_FORM_DATA]))
+                         {DOES_NOT_EXIST, INVALID_FORM_DATA})
 
-    def test_webapi_request_fields_checks_inheritance(self):
+    def test_webapi_request_fields_checks_inheritance(self) -> None:
         """Testing @webapi_request_fields properly checks inherited fields"""
-        class A(object):
+        class A:
             @webapi_request_fields(required={
                 'required1': {
                     'type': BooleanFieldType,
                     'description': 'Required param',
                 },
             })
-            def test(*args, **kwargs):
+            def test(*args, **kwargs) -> None:
                 pass
 
         class B(A):
@@ -395,7 +397,7 @@ class WebAPIDecoratorTests(TestCase):
                 },
             })
             @augment_method_from(A)
-            def test(*args, **kwargs):
+            def test(*args, **kwargs) -> None:
                 pass
 
         request = RequestFactory().post('/', data={'required2': True})
@@ -409,11 +411,11 @@ class WebAPIDecoratorTests(TestCase):
                          ['This field is required'])
         self.assertFalse('required2' in response['fields'])
 
-    def test_webapi_request_fields_call_normalizes_params(self):
+    def test_webapi_request_fields_call_normalizes_params(self) -> None:
         """Testing @webapi_request_fields normalizes params to function"""
         if not hasattr(__builtins__, 'file'):
             # Python 3.x doesn't have `file`, so define our own for the test.
-            class file(object):
+            class file:
                 pass
 
         @webapi_request_fields(
@@ -438,7 +440,7 @@ class WebAPIDecoratorTests(TestCase):
             optional={
                 'optional_param': {
                     'type': BooleanFieldType,
-                }
+                },
             },
         )
         def func(request, required_int=None, required_string=None,
@@ -480,24 +482,18 @@ class WebAPIDecoratorTests(TestCase):
         self.assertTrue(hasattr(func, 'seen'))
         self.assertIsNone(result)
 
-    def test_webapi_request_fields_call_normalizes_params_legacy_types(self):
+    def test_webapi_request_fields_call_normalizes_params_legacy_types(
+        self,
+    ) -> None:
         """Testing @webapi_request_fields normalizes params to function when
         using legacy types
         """
-        if not hasattr(__builtins__, 'file'):
-            # Python 3.x doesn't have `file`, so define our own for the test.
-            class file(object):
-                pass
-
         @webapi_request_fields(
             required={
                 'required_int': {
                     'type': int,
                 },
-                'required_unicode_str': {
-                    'type': str,
-                },
-                'required_native_str': {
+                'required_str': {
                     'type': str,
                 },
                 'required_choice_list': {
@@ -509,28 +505,22 @@ class WebAPIDecoratorTests(TestCase):
                 'required_bool': {
                     'type': bool,
                 },
-                'required_file': {
-                    'type': file,
-                },
             },
             optional={
                 'optional_param': {
                     'type': bool,
-                }
+                },
             },
         )
-        def func(request, required_int=None, required_unicode_str=None,
-                 required_native_str=None, required_choice_list=None,
-                 required_choice_tuple=None, required_file=None,
+        def func(request, required_int=None, required_str=None,
+                 required_choice_list=None, required_choice_tuple=None,
                  required_bool=None, optional_param=None,
                  parsed_request_fields=None, extra_fields={}):
             func.seen = True
             self.assertEqual(required_int, 42)
-            self.assertEqual(required_unicode_str, 'test')
-            self.assertEqual(required_native_str, str('test'))
+            self.assertEqual(required_str, 'test')
             self.assertEqual(required_choice_list, 'a')
             self.assertEqual(required_choice_tuple, 'c')
-            self.assertEqual(required_file.read(), b'content')
             self.assertTrue(required_bool)
             self.assertTrue(optional_param)
             self.assertFalse(extra_fields)
@@ -538,12 +528,10 @@ class WebAPIDecoratorTests(TestCase):
                 parsed_request_fields,
                 {
                     'required_int': required_int,
-                    'required_unicode_str': required_unicode_str,
-                    'required_native_str': required_native_str,
+                    'required_str': required_str,
                     'required_choice_list': required_choice_list,
                     'required_choice_tuple': required_choice_tuple,
                     'required_bool': required_bool,
-                    'required_file': required_file,
                     'optional_param': optional_param,
                 })
 
@@ -551,13 +539,10 @@ class WebAPIDecoratorTests(TestCase):
             path='/',
             data={
                 'required_int': '42',
-                'required_unicode_str': 'test',
-                'required_native_str': str('test'),
+                'required_str': 'test',
                 'required_choice_list': 'a',
                 'required_choice_tuple': 'c',
                 'required_bool': 'true',
-                'required_file': SimpleUploadedFile(name='test',
-                                                    content=b'content'),
                 'optional_param': '1',
             },
         ))
@@ -571,10 +556,7 @@ class WebAPIDecoratorTests(TestCase):
                 'required_int': {
                     'type': IntFieldType,
                 },
-                'required_unicode_str': {
-                    'type': StringFieldType,
-                },
-                'required_native_str': {
+                'required_str': {
                     'type': StringFieldType,
                 },
                 'required_choice_list': {
@@ -588,18 +570,15 @@ class WebAPIDecoratorTests(TestCase):
                 'required_bool': {
                     'type': BooleanFieldType,
                 },
-                'required_file': {
-                    'type': FileFieldType,
-                },
             })
 
-    def test_webapi_request_fields_call_with_unexpected_arg(self):
+    def test_webapi_request_fields_call_with_unexpected_arg(self) -> None:
         """Testing @webapi_request_fields with unexpected argument"""
         @webapi_request_fields(
             required={
                 'required_param': {
                     'type': IntFieldType,
-                }
+                },
             },
         )
         def func(request, required_param=None, extra_fields={}):
@@ -610,7 +589,7 @@ class WebAPIDecoratorTests(TestCase):
             data={
                 'required_param': '42',
                 'optional_param': '1',
-            }
+            },
         ))
 
         self.assertFalse(hasattr(func, 'seen'))
@@ -624,9 +603,9 @@ class WebAPIDecoratorTests(TestCase):
             required={
                 'required_param': {
                     'type': IntFieldType,
-                }
+                },
             },
-            allow_unknown=True
+            allow_unknown=True,
         )
         def func(request, required_param=None, parsed_request_fields=None,
                  extra_fields={}):
@@ -642,19 +621,19 @@ class WebAPIDecoratorTests(TestCase):
             data={
                 'required_param': '42',
                 'optional_param': '1',
-            }
+            },
         ))
 
         self.assertTrue(hasattr(func, 'seen'))
         self.assertEqual(result, None)
 
-    def test_webapi_request_fields_call_filter_special_params(self):
+    def test_webapi_request_fields_call_filter_special_params(self) -> None:
         """Testing @webapi_request_fields filters special params"""
         @webapi_request_fields(
             required={
                 'required_param': {
                     'type': IntFieldType,
-                }
+                },
             },
         )
         def func(request, required_param=None, parsed_request_fields=None,
@@ -671,20 +650,20 @@ class WebAPIDecoratorTests(TestCase):
             data={
                 'required_param': '42',
                 'api_format': 'json',
-            }
+            },
         ))
 
         self.assertTrue(hasattr(func, 'seen'))
         self.assertEqual(result, None)
 
-    def test_webapi_request_fields_call_validation_int(self):
+    def test_webapi_request_fields_call_validation_int(self) -> None:
         """Testing @webapi_request_fields with int parameter validation"""
         @webapi_request_fields(
             required={
                 'myint': {
                     'type': IntFieldType,
-                }
-            }
+                },
+            },
         )
         def func(request, myint=False, parsed_request_fields=None,
                  extra_fields={}):
@@ -694,9 +673,31 @@ class WebAPIDecoratorTests(TestCase):
             path='/',
             data={
                 'myint': 'abc',
-            }
+            },
         ))
         self.assertFalse(hasattr(func, 'seen'))
         self.assertEqual(result[0], INVALID_FORM_DATA)
         self.assertTrue('fields' in result[1])
         self.assertTrue('myint' in result[1]['fields'])
+
+    def test_webapi_docs_preserves_state(self) -> None:
+        """Testing @webapi_docs preserves decorator state"""
+
+        @webapi_docs('API documentation.')
+        @webapi_response_errors(DOES_NOT_EXIST)
+        def func() -> None:
+            """Codebase documentation."""
+
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertTrue(hasattr(func, 'webapi_docs'))
+        self.assertEqual(func.__annotations__, {
+            'return': 'None',
+        })
+        self.assertEqual(func.__name__, 'func')
+        self.assertEqual(func.__doc__, 'Codebase documentation.')
+        self.assertEqual(
+            func.__qualname__,
+            'WebAPIDecoratorTests.test_webapi_docs_preserves_state.'
+            '<locals>.func')
+        self.assertEqual(func.webapi_docs, 'API documentation.')
+        self.assertEqual(func.response_errors, {DOES_NOT_EXIST})
