diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 313d93f0ca..07ba3decb5 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -52,6 +52,7 @@ Changelog
  * Implement new tabs design across the admin interface (Steven Steinwand)
  * Move page meta information from the header to a new status side panel component inside of the page editing UI (Steven Steinwand, Karl Hobley)
  * Add useful help text to Tag fields to advise what content is allowed inside tags, including when `TAG_SPACES_ALLOWED` is `True` or `False` (Abdulmajeed Isa)
+ * Change `AbstractFormSubmission`'s `form_data` to use JSONField to store form submissions (Jake Howard)
  * Fix: When using `simple_translations` ensure that the user is redirected to the page edit view when submitting for a single locale (Mitchel Cabuloy)
  * Fix: When previewing unsaved changes to `Form` pages, ensure that all added fields are correctly shown in the preview (Joshua Munn)
  * Fix: When Documents (e.g. PDFs) have been configured to be served inline via `WAGTAILDOCS_CONTENT_TYPES` & `WAGTAILDOCS_INLINE_CONTENT_TYPES` ensure that the filename is correctly set in the `Content-Disposition` header so that saving the files will use the correct filename (John-Scott Atlakson)
diff --git a/docs/reference/contrib/forms/customisation.md b/docs/reference/contrib/forms/customisation.md
index 21897c1092..54cec58bfd 100644
--- a/docs/reference/contrib/forms/customisation.md
+++ b/docs/reference/contrib/forms/customisation.md
@@ -58,7 +58,6 @@ Example:
 import json
 
 from django.conf import settings
-from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from modelcluster.fields import ParentalKey
 from wagtail.admin.panels import (
@@ -95,7 +94,7 @@ class FormPage(AbstractEmailForm):
 
     def process_form_submission(self, form):
         self.get_submission_class().objects.create(
-            form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data,
             page=self, user=form.user
         )
 
@@ -118,7 +117,6 @@ Note that this code also changes the submissions list view.
 import json
 
 from django.conf import settings
-from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from modelcluster.fields import ParentalKey
 from wagtail.admin.panels import (
@@ -163,7 +161,7 @@ class FormPage(AbstractEmailForm):
 
     def process_form_submission(self, form):
         self.get_submission_class().objects.create(
-            form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data,
             page=self, user=form.user
         )
 
@@ -191,7 +189,6 @@ Example:
 import json
 
 from django.conf import settings
-from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.shortcuts import render
 from modelcluster.fields import ParentalKey
@@ -239,7 +236,7 @@ class FormPage(AbstractEmailForm):
 
     def process_form_submission(self, form):
         self.get_submission_class().objects.create(
-            form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data,
             page=self, user=form.user
         )
 
diff --git a/docs/releases/3.0.md b/docs/releases/3.0.md
index 0ba56b676e..da7c472886 100644
--- a/docs/releases/3.0.md
+++ b/docs/releases/3.0.md
@@ -81,6 +81,7 @@ class LandingPage(Page):
  * Add internationalisation UI to modeladmin (Andrés Martano)
  * Support chunking in `PageQuerySet.specific()` to reduce memory consumption (Andy Babic)
  * Add useful help text to Tag fields to advise what content is allowed inside tags, including when `TAG_SPACES_ALLOWED` is `True` or `False` (Abdulmajeed Isa)
+ * Change `AbstractFormSubmission`'s `form_data` to use JSONField to store form submissions (Jake Howard)
  * Fix: Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)
 
 ### Bug fixes
@@ -206,6 +207,20 @@ The `content_json` field in the `PageRevision` model has been renamed to `conten
 
 The `data_json` field in the `BaseLogEntry` model (and its subclasses `PageLogEntry` and `ModelLogEntry`) has been renamed to `data`, and this field now internally uses `JSONField` instead of `TextField`. If you have a large number of objects for these models, running the migrations might take a while.
 
+### Replaced `form_data` `TextField` with `JSONField` in `AbstractFormSubmission`
+
+The `form_data` field in the `AbstractFormSubmission` model (and its subclasses `FormSubmission`) has been converted to `JSONField` instead of `TextField`. If you have customisations that programmatically add form submissions you will need to ensure that the `form_data` that is output is no longer a JSON string but instead a serialisable Python object. When interacting with the `form_data` you will now receive a Python object and not a string.
+
+Example change
+```
+    def process_form_submission(self, form):
+        self.get_submission_class().objects.create(
+            # form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data, # new
+            page=self, user=form.user
+        )
+```
+
 ### Removed `size` argument from `wagtail.utils.sendfile_streaming_backend.was_modified_since`
 
 The `size` argument of the undocumented `wagtail.utils.sendfile_streaming_backend.was_modified_since` function has been removed. This argument was used to add a `length` parameter to the HTTP header; however, this was never part of the HTTP/1.0 and HTTP/1.1 specifications see [RFC7232](https://httpwg.org/specs/rfc7232.html#header.if-modified-since) and existed only as a an unofficial implementation in IE browsers.
diff --git a/wagtail/contrib/forms/migrations/0005_alter_formsubmission_form_data.py b/wagtail/contrib/forms/migrations/0005_alter_formsubmission_form_data.py
new file mode 100644
index 0000000000..c6757c2c01
--- /dev/null
+++ b/wagtail/contrib/forms/migrations/0005_alter_formsubmission_form_data.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.0.3 on 2022-03-28 11:59
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailforms", "0004_add_verbose_name_plural"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="formsubmission",
+            name="form_data",
+            field=models.JSONField(
+                encoder=django.core.serializers.json.DjangoJSONEncoder
+            ),
+        ),
+    ]
diff --git a/wagtail/contrib/forms/models.py b/wagtail/contrib/forms/models.py
index 4b72bb6844..a268b6f919 100644
--- a/wagtail/contrib/forms/models.py
+++ b/wagtail/contrib/forms/models.py
@@ -1,5 +1,4 @@
 import datetime
-import json
 import os
 
 from django.conf import settings
@@ -42,7 +41,7 @@ class AbstractFormSubmission(models.Model):
     For example, if you need to save additional data or a reference to a user.
     """
 
-    form_data = models.TextField()
+    form_data = models.JSONField(encoder=DjangoJSONEncoder)
     page = models.ForeignKey(Page, on_delete=models.CASCADE)
 
     submit_time = models.DateTimeField(verbose_name=_("submit time"), auto_now_add=True)
@@ -53,14 +52,11 @@ class AbstractFormSubmission(models.Model):
 
         You can override this method to add additional data.
         """
-        form_data = json.loads(self.form_data)
-        form_data.update(
-            {
-                "submit_time": self.submit_time,
-            }
-        )
 
-        return form_data
+        return {
+            **self.form_data,
+            "submit_time": self.submit_time,
+        }
 
     def __str__(self):
         return self.form_data
@@ -231,7 +227,7 @@ class AbstractForm(Page):
         """
 
         return self.get_submission_class().objects.create(
-            form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data,
             page=self,
         )
 
diff --git a/wagtail/contrib/forms/tests/test_models.py b/wagtail/contrib/forms/tests/test_models.py
index d41ed3263a..5a17dbc28b 100644
--- a/wagtail/contrib/forms/tests/test_models.py
+++ b/wagtail/contrib/forms/tests/test_models.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-import json
 import unittest
 
 from django import VERSION as DJANGO_VERSION
@@ -109,7 +108,7 @@ class TestFormSubmission(TestCase):
         form_page = Page.objects.get(url_path="/home/contact-us/")
         self.assertTrue(
             FormSubmission.objects.filter(
-                page=form_page, form_data__contains="hello world"
+                page=form_page, form_data__your_message="hello world"
             ).exists()
         )
 
@@ -129,8 +128,7 @@ class TestFormSubmission(TestCase):
 
         # Check the form submission
         submission = FormSubmission.objects.get()
-        submission_data = json.loads(submission.form_data)
-        self.assertEqual(submission_data["your_message"], "こんにちは、世界")
+        self.assertEqual(submission.form_data["your_message"], "こんにちは、世界")
 
     def test_post_multiple_values(self):
         response = self.client.post(
@@ -150,11 +148,9 @@ class TestFormSubmission(TestCase):
         # Check that the three checkbox values were saved correctly
         form_page = Page.objects.get(url_path="/home/contact-us/")
         submission = FormSubmission.objects.filter(
-            page=form_page, form_data__contains="hello world"
+            page=form_page, form_data__your_message="hello world"
         )
-        self.assertIn("foo", submission[0].form_data)
-        self.assertIn("bar", submission[0].form_data)
-        self.assertIn("baz", submission[0].form_data)
+        self.assertEqual(submission[0].form_data["your_choices"], ["foo", "bar", "baz"])
 
         # Check that the all the multiple checkbox values are serialised in the
         # email correctly
@@ -279,7 +275,7 @@ class TestFormWithCustomSubmission(TestCase, WagtailTestUtils):
         form_page = Page.objects.get(url_path="/home/contact-us/")
         self.assertTrue(
             CustomFormPageSubmission.objects.filter(
-                page=form_page, form_data__contains="hello world"
+                page=form_page, form_data__your_message="hello world"
             ).exists()
         )
 
@@ -312,7 +308,7 @@ class TestFormWithCustomSubmission(TestCase, WagtailTestUtils):
         )
         self.assertEqual(submissions_qs.count(), 1)
         self.assertTrue(
-            submissions_qs.filter(form_data__contains="hello world").exists()
+            submissions_qs.filter(form_data__your_message="hello world").exists()
         )
 
         # Second submission
@@ -344,12 +340,7 @@ class TestFormWithCustomSubmission(TestCase, WagtailTestUtils):
             user=self.user, page=self.form_page
         )
         self.assertEqual(submissions_qs.count(), 1)
-        self.assertTrue(
-            submissions_qs.filter(form_data__contains="hello world").exists()
-        )
-        self.assertFalse(
-            submissions_qs.filter(form_data__contains="hello cruel world").exists()
-        )
+        self.assertEqual(submissions_qs.get().form_data["your_message"], "hello world")
 
     def test_post_unicode_characters(self):
         self.client.post(
@@ -367,8 +358,7 @@ class TestFormWithCustomSubmission(TestCase, WagtailTestUtils):
 
         # Check the form submission
         submission = CustomFormPageSubmission.objects.get()
-        submission_data = json.loads(submission.form_data)
-        self.assertEqual(submission_data["your_message"], "こんにちは、世界")
+        self.assertEqual(submission.form_data["your_message"], "こんにちは、世界")
 
     def test_post_multiple_values(self):
         response = self.client.post(
@@ -392,11 +382,10 @@ class TestFormWithCustomSubmission(TestCase, WagtailTestUtils):
         # Check that the three checkbox values were saved correctly
         form_page = Page.objects.get(url_path="/home/contact-us/")
         submission = CustomFormPageSubmission.objects.filter(
-            page=form_page, form_data__contains="hello world"
+            page=form_page, form_data__your_message="hello world"
         )
-        self.assertIn("foo", submission[0].form_data)
-        self.assertIn("bar", submission[0].form_data)
-        self.assertIn("baz", submission[0].form_data)
+
+        self.assertEqual(submission[0].form_data["your_choices"], ["foo", "bar", "baz"])
 
     def test_post_blank_checkbox(self):
         response = self.client.post(
@@ -464,7 +453,7 @@ class TestFormSubmissionWithMultipleRecipients(TestCase):
         form_page = Page.objects.get(url_path="/home/contact-us/")
         self.assertTrue(
             FormSubmission.objects.filter(
-                page=form_page, form_data__contains="hello world"
+                page=form_page, form_data__your_message="hello world"
             ).exists()
         )
 
@@ -514,7 +503,7 @@ class TestFormSubmissionWithMultipleRecipientsAndWithCustomSubmission(
         form_page = Page.objects.get(url_path="/home/contact-us/")
         self.assertTrue(
             CustomFormPageSubmission.objects.filter(
-                page=form_page, form_data__contains="hello world"
+                page=form_page, form_data__your_message="hello world"
             ).exists()
         )
 
@@ -551,7 +540,7 @@ class TestFormWithRedirect(TestCase):
         form_page = Page.objects.get(url_path="/home/contact-us/")
         self.assertTrue(
             FormSubmission.objects.filter(
-                page=form_page, form_data__contains="hello world"
+                page=form_page, form_data__your_message="hello world"
             ).exists()
         )
 
@@ -801,12 +790,12 @@ class TestIssue798(TestCase, WagtailTestUtils):
         # Check that form submission was saved correctly
         self.assertTrue(
             FormSubmission.objects.filter(
-                page=self.form_page, form_data__contains="hello world"
+                page=self.form_page, form_data__your_message="hello world"
             ).exists()
         )
         self.assertTrue(
             FormSubmission.objects.filter(
-                page=self.form_page, form_data__contains="7.3"
+                page=self.form_page, form_data__your_favourite_number="7.3"
             ).exists()
         )
 
diff --git a/wagtail/contrib/forms/tests/test_views.py b/wagtail/contrib/forms/tests/test_views.py
index e4f2df4ceb..60cc70c719 100644
--- a/wagtail/contrib/forms/tests/test_views.py
+++ b/wagtail/contrib/forms/tests/test_views.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
 import datetime
-import json
 from io import BytesIO
 
 from django.conf import settings
@@ -102,13 +101,11 @@ class TestFormResponsesPanelWithCustomSubmissionClass(TestCase, WagtailTestUtils
         new_form_submission = CustomFormPageSubmission.objects.create(
             user=self.test_user,
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "email@domain.com",
-                    "your_message": "hi joe",
-                    "your_choices": {"foo": "", "bar": "", "baz": ""},
-                }
-            ),
+            form_data={
+                "your_email": "email@domain.com",
+                "your_message": "hi joe",
+                "your_choices": {"foo": "", "bar": "", "baz": ""},
+            },
         )
         new_form_submission.submit_time = "2017-08-29T12:00:00.000Z"
         new_form_submission.save()
@@ -341,25 +338,21 @@ class TestFormsSubmissionsList(TestCase, WagtailTestUtils):
 
         new_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "your_message": "this is a fairly new message",
-                    "your_choices": ["foo", "baz"],
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "your_message": "this is a fairly new message",
+                "your_choices": ["foo", "baz"],
+            },
         )
         new_form_submission.submit_time = "2014-01-01T12:00:00.000Z"
         new_form_submission.save()
 
         old_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "old@example.com",
-                    "your_message": "this is a really old message",
-                }
-            ),
+            form_data={
+                "your_email": "old@example.com",
+                "your_message": "this is a really old message",
+            },
         )
         old_form_submission.submit_time = "2013-01-01T12:00:00.000Z"
         old_form_submission.save()
@@ -373,7 +366,7 @@ class TestFormsSubmissionsList(TestCase, WagtailTestUtils):
         """
         for i in range(100):
             submission = FormSubmission(
-                page=self.form_page, form_data=json.dumps({"hello": "world"})
+                page=self.form_page, form_data={"hello": "world"}
             )
             submission.save()
 
@@ -529,13 +522,11 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
         # Add a couple of form submissions
         old_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "old@example.com",
-                    "your_message": "this is a really old message",
-                    "your_choices": ["foo", "baz"],
-                }
-            ),
+            form_data={
+                "your_email": "old@example.com",
+                "your_message": "this is a really old message",
+                "your_choices": ["foo", "baz"],
+            },
         )
         if settings.USE_TZ:
             old_form_submission.submit_time = "2013-01-01T12:00:00.000Z"
@@ -545,12 +536,10 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
 
         new_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "your_message": "this is a fairly new message",
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "your_message": "this is a fairly new message",
+            },
         )
         if settings.USE_TZ:
             new_form_submission.submit_time = "2014-01-01T12:00:00.000Z"
@@ -631,12 +620,10 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
         for i in range(100):
             new_form_submission = FormSubmission.objects.create(
                 page=self.form_page,
-                form_data=json.dumps(
-                    {
-                        "your-email": "new@example-%s.com" % i,
-                        "your-message": "I like things x %s" % i,
-                    }
-                ),
+                form_data={
+                    "your-email": "new@example-%s.com" % i,
+                    "your-message": "I like things x %s" % i,
+                },
             )
             if settings.USE_TZ:
                 new_form_submission.submit_time = "2014-01-01T12:00:00.000Z"
@@ -778,12 +765,10 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
     def test_list_submissions_csv_export_with_unicode_in_submission(self):
         unicode_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "unicode@example.com",
-                    "your_message": "こんにちは、世界",
-                }
-            ),
+            form_data={
+                "your_email": "unicode@example.com",
+                "your_message": "こんにちは、世界",
+            },
         )
         unicode_form_submission.submit_time = "2014-01-02T12:00:00.000Z"
         unicode_form_submission.save()
@@ -810,13 +795,11 @@ class TestFormsSubmissionsExport(TestCase, WagtailTestUtils):
         )
         unicode_form_submission = FormSubmission.objects.create(
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "unicode@example.com",
-                    "your_message": "We don't need unicode here",
-                    "u0412u044bu0431u0435u0440u0438u0442u0435_u0441u0430u043cu0443u044e_u043bu044eu0431u0438u043cu0443u044e_ide_u0434u043bu044f_u0440u0430u0437u0440u0430u0431u043eu0442u043au0435_u043du0430_python": "vim",
-                }
-            ),
+            form_data={
+                "your_email": "unicode@example.com",
+                "your_message": "We don't need unicode here",
+                "u0412u044bu0431u0435u0440u0438u0442u0435_u0441u0430u043cu0443u044e_u043bu044eu0431u0438u043cu0443u044e_ide_u0434u043bu044f_u0440u0430u0437u0440u0430u0431u043eu0442u043au0435_u043du0430_python": "vim",
+            },
         )
         unicode_form_submission.submit_time = "2014-01-02T12:00:00.000Z"
         unicode_form_submission.save()
@@ -848,12 +831,10 @@ class TestCustomFormsSubmissionsExport(TestCase, WagtailTestUtils):
         old_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-john"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "old@example.com",
-                    "your_message": "this is a really old message",
-                }
-            ),
+            form_data={
+                "your_email": "old@example.com",
+                "your_message": "this is a really old message",
+            },
         )
         if settings.USE_TZ:
             old_form_submission.submit_time = "2013-01-01T12:00:00.000Z"
@@ -864,12 +845,10 @@ class TestCustomFormsSubmissionsExport(TestCase, WagtailTestUtils):
         new_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-m1kola"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "your_message": "this is a fairly new message",
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "your_message": "this is a fairly new message",
+            },
         )
         if settings.USE_TZ:
             new_form_submission.submit_time = "2014-01-01T12:00:00.000Z"
@@ -992,12 +971,10 @@ class TestCustomFormsSubmissionsExport(TestCase, WagtailTestUtils):
         unicode_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-bob"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "unicode@example.com",
-                    "your_message": "こんにちは、世界",
-                }
-            ),
+            form_data={
+                "your_email": "unicode@example.com",
+                "your_message": "こんにちは、世界",
+            },
         )
         unicode_form_submission.submit_time = "2014-01-02T12:00:00.000Z"
         unicode_form_submission.save()
@@ -1025,13 +1002,11 @@ class TestCustomFormsSubmissionsExport(TestCase, WagtailTestUtils):
         unicode_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-bob"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your-email": "unicode@example.com",
-                    "your-message": "We don't need unicode here",
-                    "u0412u044bu0431u0435u0440u0438u0442u0435_u0441u0430u043cu0443u044e_u043bu044eu0431u0438u043cu0443u044e_ide_u0434u043bu044f_u0440u0430u0437u0440u0430u0431u043eu0442u043au0435_u043du0430_python": "vim",
-                }
-            ),
+            form_data={
+                "your-email": "unicode@example.com",
+                "your-message": "We don't need unicode here",
+                "u0412u044bu0431u0435u0440u0438u0442u0435_u0441u0430u043cu0443u044e_u043bu044eu0431u0438u043cu0443u044e_ide_u0434u043bu044f_u0440u0430u0437u0440u0430u0431u043eu0442u043au0435_u043du0430_python": "vim",
+            },
         )
         unicode_form_submission.submit_time = "2014-01-02T12:00:00.000Z"
         unicode_form_submission.save()
@@ -1063,12 +1038,10 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
         old_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-john"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "old@example.com",
-                    "your_message": "this is a really old message",
-                }
-            ),
+            form_data={
+                "your_email": "old@example.com",
+                "your_message": "this is a really old message",
+            },
         )
         old_form_submission.submit_time = "2013-01-01T12:00:00.000Z"
         old_form_submission.save()
@@ -1076,12 +1049,10 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
         new_form_submission = CustomFormPageSubmission.objects.create(
             user=self.create_test_user_without_admin("user-m1kola"),
             page=self.form_page,
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "your_message": "this is a fairly new message",
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "your_message": "this is a fairly new message",
+            },
         )
         new_form_submission.submit_time = "2014-01-01T12:00:00.000Z"
         new_form_submission.save()
@@ -1097,12 +1068,10 @@ class TestCustomFormsSubmissionsList(TestCase, WagtailTestUtils):
             submission = CustomFormPageSubmission(
                 user=self.create_test_user_without_admin("generated-username-%s" % i),
                 page=self.form_page,
-                form_data=json.dumps(
-                    {
-                        "your_email": "generated-your-email-%s" % i,
-                        "your_message": "generated-your-message-%s" % i,
-                    }
-                ),
+                form_data={
+                    "your_email": "generated-your-email-%s" % i,
+                    "your_message": "generated-your-message-%s" % i,
+                },
             )
             submission.save()
 
@@ -1452,14 +1421,12 @@ class TestFormsWithCustomSubmissionsList(TestCase, WagtailTestUtils):
         new_form_submission = CustomFormPageSubmission.objects.create(
             page=self.form_page,
             user=self.test_user_1,
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "chocolate": "White Chocolate",
-                    "ingredients": "White colouring",
-                    "your_excitement": self.choices[2],
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "chocolate": "White Chocolate",
+                "ingredients": "White colouring",
+                "your_excitement": self.choices[2],
+            },
         )
         if settings.USE_TZ:
             new_form_submission.submit_time = "2017-10-01T12:00:00.000Z"
@@ -1470,14 +1437,12 @@ class TestFormsWithCustomSubmissionsList(TestCase, WagtailTestUtils):
         old_form_submission = CustomFormPageSubmission.objects.create(
             page=self.form_page,
             user=self.test_user_2,
-            form_data=json.dumps(
-                {
-                    "your_email": "old@example.com",
-                    "chocolate": "Dark Chocolate",
-                    "ingredients": "Charcoal",
-                    "your_excitement": self.choices[0],
-                }
-            ),
+            form_data={
+                "your_email": "old@example.com",
+                "chocolate": "Dark Chocolate",
+                "ingredients": "Charcoal",
+                "your_excitement": self.choices[0],
+            },
         )
         if settings.USE_TZ:
             old_form_submission.submit_time = "2017-01-01T12:00:00.000Z"
@@ -1493,13 +1458,11 @@ class TestFormsWithCustomSubmissionsList(TestCase, WagtailTestUtils):
             submission = CustomFormPageSubmission(
                 page=self.form_page,
                 user=self.test_user_1,
-                form_data=json.dumps(
-                    {
-                        "your_email": "foo-%s@bar.com" % i,
-                        "chocolate": "Chocolate No.%s" % i,
-                        "your_excitement": self.choices[3],
-                    }
-                ),
+                form_data={
+                    "your_email": "foo-%s@bar.com" % i,
+                    "chocolate": "Chocolate No.%s" % i,
+                    "your_excitement": self.choices[3],
+                },
             )
             submission.save()
 
@@ -1575,14 +1538,12 @@ class TestFormsWithCustomSubmissionsList(TestCase, WagtailTestUtils):
         form_submission = CustomFormPageSubmission.objects.create(
             page=self.form_page,
             user=self.create_test_user_without_admin("user-aaa-aaa"),
-            form_data=json.dumps(
-                {
-                    "your_email": "new@example.com",
-                    "chocolate": "Old chocolate idea",
-                    "ingredients": "Sugar",
-                    "your_excitement": self.choices[2],
-                }
-            ),
+            form_data={
+                "your_email": "new@example.com",
+                "chocolate": "Old chocolate idea",
+                "ingredients": "Sugar",
+                "your_excitement": self.choices[2],
+            },
         )
         form_submission.submit_time = "2016-01-01T12:00:00.000Z"
         form_submission.save()
@@ -1625,12 +1586,10 @@ class TestFormsWithCustomFormBuilderSubmissionsList(TestCase, WagtailTestUtils):
         for i in range(20):
             submission = FormSubmission.objects.create(
                 page=form_page,
-                form_data=json.dumps(
-                    {
-                        "name": "John %s" % i,
-                        "device_ip_address": "192.0.2.%s" % i,
-                    }
-                ),
+                form_data={
+                    "name": "John %s" % i,
+                    "device_ip_address": "192.0.2.%s" % i,
+                },
             )
             submission.save()
         self.form_page = form_page
diff --git a/wagtail/test/testapp/fixtures/test.json b/wagtail/test/testapp/fixtures/test.json
index 9ed2f54dc5..84baf22adb 100644
--- a/wagtail/test/testapp/fixtures/test.json
+++ b/wagtail/test/testapp/fixtures/test.json
@@ -532,7 +532,10 @@
     "pk": 1,
     "model": "tests.customformpagesubmission",
     "fields": {
-      "form_data": "{\"your-email\": \"old@example.com\", \"your-message\": \"this is a really old message\"}",
+      "form_data": {
+        "your-email": "old@example.com",
+        "your-message": "this is a really old message"
+      },
       "user": 2,
       "page": 17,
       "submit_time": "2013-01-01T12:00:00.000Z"
@@ -542,7 +545,10 @@
     "pk": 2,
     "model": "tests.customformpagesubmission",
     "fields": {
-      "form_data": "{\"your-email\": \"new@example.com\", \"your-message\": \"this is a fairly new message\"}",
+      "form_data": {
+        "your-email": "new@example.com",
+        "your-message": "this is a fairly new message"
+      },
       "user": 5,
       "page": 17,
       "submit_time": "2014-01-01T12:00:00.000Z"
@@ -791,7 +797,10 @@
     "pk": 1,
     "model": "wagtailforms.formsubmission",
     "fields": {
-      "form_data": "{\"your_email\": \"old@example.com\", \"your_message\": \"this is a really old message\"}",
+      "form_data": {
+        "your_email": "old@example.com",
+        "your_message": "this is a really old message"
+      },
       "page": 8,
       "submit_time": "2013-01-01T12:00:00.000Z"
     }
@@ -800,7 +809,10 @@
     "pk": 2,
     "model": "wagtailforms.formsubmission",
     "fields": {
-      "form_data": "{\"your_email\": \"new@example.com\", \"your_message\": \"this is a fairly new message\"}",
+      "form_data": {
+        "your_email": "new@example.com",
+        "your_message": "this is a fairly new message"
+      },
       "page": 8,
       "submit_time": "2014-01-01T12:00:00.000Z"
     }
diff --git a/wagtail/test/testapp/migrations/0067_alter_customformpagesubmission_form_data.py b/wagtail/test/testapp/migrations/0067_alter_customformpagesubmission_form_data.py
new file mode 100644
index 0000000000..609cbc57ca
--- /dev/null
+++ b/wagtail/test/testapp/migrations/0067_alter_customformpagesubmission_form_data.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.0.3 on 2022-03-28 11:59
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("tests", "0066_pagewithgenericrelation_relatedgenericrelation"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="customformpagesubmission",
+            name="form_data",
+            field=models.JSONField(
+                encoder=django.core.serializers.json.DjangoJSONEncoder
+            ),
+        ),
+    ]
diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py
index f7a8d38315..f213eb811b 100644
--- a/wagtail/test/testapp/models.py
+++ b/wagtail/test/testapp/models.py
@@ -1,5 +1,4 @@
 import hashlib
-import json
 import os
 import uuid
 
@@ -9,7 +8,6 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
-from django.core.serializers.json import DjangoJSONEncoder
 from django.db import models
 from django.shortcuts import redirect
 from django.template.response import TemplateResponse
@@ -646,7 +644,7 @@ class FormPageWithCustomSubmission(AbstractEmailForm):
 
     def process_form_submission(self, form):
         form_submission = self.get_submission_class().objects.create(
-            form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
+            form_data=form.cleaned_data,
             page=self,
             user=form.user,
         )