← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:remove-simplejson into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:remove-simplejson into launchpad:master.

Commit message:
Remove simplejson dependency

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/431994

The only things that `simplejson` still gave us compared with the standard library's `json` were more consistently accepting both bytes and text input across all supported Python versions.  It isn't really worth a whole dependency just for that.

I normalized how `json.dumps` and `json.loads` are spelled; this makes the diff longer (sorry), but it makes it easier to grep for certain patterns.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:remove-simplejson into launchpad:master.
diff --git a/lib/lp/answers/browser/questionsubscription.py b/lib/lp/answers/browser/questionsubscription.py
index cc2ea58..990e1a3 100644
--- a/lib/lp/answers/browser/questionsubscription.py
+++ b/lib/lp/answers/browser/questionsubscription.py
@@ -7,9 +7,10 @@ __all__ = [
     "QuestionPortletSubscribersWithDetails",
 ]
 
+import json
+
 from lazr.delegates import delegate_to
 from lazr.restful.interfaces import IWebServiceClientRequest
-from simplejson import dumps
 from zope.traversing.browser import absoluteURL
 
 from lp.answers.interfaces.question import IQuestion
@@ -78,7 +79,7 @@ class QuestionPortletSubscribersWithDetails(LaunchpadView):
                 "subscription_level": "Indirect",
             }
             data.append(record)
-        return dumps(data)
+        return json.dumps(data)
 
     def render(self):
         """Override the default render() to return only JSON."""
diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
index bbb6d26..8682cf9 100644
--- a/lib/lp/answers/browser/questiontarget.py
+++ b/lib/lp/answers/browser/questiontarget.py
@@ -18,11 +18,11 @@ __all__ = [
     "UserSupportLanguagesMixin",
 ]
 
+import json
 from operator import attrgetter
 from urllib.parse import urlencode
 
 from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
-from simplejson import dumps
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getMultiAdapter, getUtility, queryMultiAdapter
 from zope.formlib import form
@@ -965,7 +965,7 @@ class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
         """Return subscriber_ids in a form suitable for JavaScript use."""
         questiontarget = IQuestionTarget(self.context)
         data = self.answercontact_data(questiontarget)
-        return dumps(data)
+        return json.dumps(data)
 
     def render(self):
         """Override the default render() to return only JSON."""
diff --git a/lib/lp/answers/model/questionjob.py b/lib/lp/answers/model/questionjob.py
index 2f2d794..3a7277c 100644
--- a/lib/lp/answers/model/questionjob.py
+++ b/lib/lp/answers/model/questionjob.py
@@ -7,7 +7,8 @@ __all__ = [
     "QuestionJob",
 ]
 
-import simplejson
+import json
+
 import six
 from lazr.delegates import delegate_to
 from storm.expr import And
@@ -70,7 +71,7 @@ class QuestionJob(StormBase):
         self.job = Job()
         self.job_type = job_type
         self.question = question
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         self._json_data = six.ensure_text(json_data)
 
     def __repr__(self):
@@ -82,7 +83,7 @@ class QuestionJob(StormBase):
     @property
     def metadata(self):
         """See `IQuestionJob`."""
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     def makeDerived(self):
         if self.job_type != QuestionJobType.EMAIL:
diff --git a/lib/lp/answers/tests/test_question_webservice.py b/lib/lp/answers/tests/test_question_webservice.py
index 2acee73..1d92cc7 100644
--- a/lib/lp/answers/tests/test_question_webservice.py
+++ b/lib/lp/answers/tests/test_question_webservice.py
@@ -3,10 +3,10 @@
 
 """Webservice unit tests related to Launchpad Questions."""
 
+import json
 from datetime import datetime, timedelta
 
 import pytz
-from simplejson import dumps
 from testtools.matchers import EndsWith
 
 from lp.answers.enums import QuestionStatus
@@ -132,7 +132,7 @@ class TestQuestionRepresentation(TestCaseWithFactory):
         response = self.webservice.patch(
             question_json["self_link"],
             "application/json",
-            dumps(dict(title=new_title)),
+            json.dumps(dict(title=new_title)),
             headers=dict(accept="application/xhtml+xml"),
         )
 
diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
index 01ed055..126eb3f 100644
--- a/lib/lp/app/browser/launchpadform.py
+++ b/lib/lp/app/browser/launchpadform.py
@@ -14,9 +14,9 @@ __all__ = [
     "safe_action",
 ]
 
+import json
 from typing import List, Optional, Type
 
-import simplejson
 import transaction
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
@@ -140,7 +140,7 @@ class LaunchpadFormView(LaunchpadView):
         ]
         if notifications:
             request.response.setHeader(
-                "X-Lazr-Notifications", simplejson.dumps(notifications)
+                "X-Lazr-Notifications", json.dumps(notifications)
             )
 
     def render(self):
@@ -387,7 +387,7 @@ class LaunchpadFormView(LaunchpadView):
             errors=errors,
             error_summary=self.error_count,
         )
-        return simplejson.dumps(return_data)
+        return json.dumps(return_data)
 
     def validate(self, data):
         """Validate the form.
diff --git a/lib/lp/app/browser/lazrjs.py b/lib/lp/app/browser/lazrjs.py
index 23a6bc0..2d3a6f3 100644
--- a/lib/lp/app/browser/lazrjs.py
+++ b/lib/lp/app/browser/lazrjs.py
@@ -15,7 +15,8 @@ __all__ = [
     "vocabulary_to_choice_edit_items",
 ]
 
-import simplejson
+import json
+
 from lazr.enum import IEnumeratedType
 from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
 from lazr.restful.utils import get_current_browser_request
@@ -81,7 +82,7 @@ class WidgetBase:
             if mutator_info is not None:
                 mutator_method, mutator_extra = mutator_info
                 self.mutator_method_name = mutator_method.__name__
-        self.json_attribute = simplejson.dumps(self.api_attribute)
+        self.json_attribute = json.dumps(self.api_attribute)
 
     @property
     def resource_uri(self):
@@ -95,7 +96,7 @@ class WidgetBase:
 
     @property
     def json_resource_uri(self):
-        return simplejson.dumps(self.resource_uri)
+        return json.dumps(self.resource_uri)
 
     @property
     def can_write(self):
@@ -132,13 +133,13 @@ class TextWidgetBase(WidgetBase):
             edit_url,
             edit_title,
         )
-        self.accept_empty = simplejson.dumps(self.optional_field)
+        self.accept_empty = json.dumps(self.optional_field)
         self.title = title
-        self.widget_css_selector = simplejson.dumps("#" + self.content_box_id)
+        self.widget_css_selector = json.dumps("#" + self.content_box_id)
 
     @property
     def json_attribute_uri(self):
-        return simplejson.dumps(self.resource_uri + "/" + self.api_attribute)
+        return json.dumps(self.resource_uri + "/" + self.api_attribute)
 
 
 class DefinedTagMixin:
@@ -221,8 +222,8 @@ class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
         self.max_width = max_width
         self.truncate_lines = truncate_lines
         self.default_text = default_text
-        self.initial_value_override = simplejson.dumps(initial_value_override)
-        self.width = simplejson.dumps(width)
+        self.initial_value_override = json.dumps(initial_value_override)
+        self.width = json.dumps(width)
 
     @property
     def value(self):
@@ -364,9 +365,9 @@ class InlineEditPickerWidget(WidgetBase):
         self.help_link = help_link
 
         # JSON encoded attributes.
-        self.json_content_box_id = simplejson.dumps(self.content_box_id)
-        self.json_attribute = simplejson.dumps(self.api_attribute + "_link")
-        self.json_vocabulary_name = simplejson.dumps(
+        self.json_content_box_id = json.dumps(self.content_box_id)
+        self.json_attribute = json.dumps(self.api_attribute + "_link")
+        self.json_vocabulary_name = json.dumps(
             self.exported_field.vocabularyName
         )
 
@@ -409,7 +410,7 @@ class InlineEditPickerWidget(WidgetBase):
 
     @property
     def json_config(self):
-        return simplejson.dumps(self.config)
+        return json.dumps(self.config)
 
     @cachedproperty
     def vocabulary(self):
@@ -628,13 +629,11 @@ class InlineMultiCheckboxWidget(WidgetBase):
         self.has_choices = len(items)
 
         # JSON encoded attributes.
-        self.json_content_box_id = simplejson.dumps(self.content_box_id)
-        self.json_attribute = simplejson.dumps(self.api_attribute)
-        self.json_attribute_type = simplejson.dumps(attribute_type)
-        self.json_items = simplejson.dumps(items)
-        self.json_description = simplejson.dumps(
-            self.exported_field.description
-        )
+        self.json_content_box_id = json.dumps(self.content_box_id)
+        self.json_attribute = json.dumps(self.api_attribute)
+        self.json_attribute_type = json.dumps(attribute_type)
+        self.json_items = json.dumps(items)
+        self.json_description = json.dumps(self.exported_field.description)
 
     @property
     def config(self):
@@ -644,7 +643,7 @@ class InlineMultiCheckboxWidget(WidgetBase):
 
     @property
     def json_config(self):
-        return simplejson.dumps(self.config)
+        return json.dumps(self.config)
 
 
 def vocabulary_to_choice_edit_items(
@@ -708,7 +707,7 @@ def vocabulary_to_choice_edit_items(
         items.append(new_item)
 
     if as_json:
-        return simplejson.dumps(items)
+        return json.dumps(items)
     else:
         return items
 
@@ -812,7 +811,7 @@ class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
 
     @property
     def json_config(self):
-        return simplejson.dumps(self.config)
+        return json.dumps(self.config)
 
 
 class EnumChoiceWidget(WidgetBase):
@@ -883,4 +882,4 @@ class EnumChoiceWidget(WidgetBase):
 
     @property
     def json_config(self):
-        return simplejson.dumps(self.config)
+        return json.dumps(self.config)
diff --git a/lib/lp/app/browser/linkchecker.py b/lib/lp/app/browser/linkchecker.py
index 2ad22eb..5eac333 100644
--- a/lib/lp/app/browser/linkchecker.py
+++ b/lib/lp/app/browser/linkchecker.py
@@ -5,7 +5,8 @@ __all__ = [
     "LinkCheckerAPI",
 ]
 
-import simplejson
+import json
+
 from zope.component import getUtility
 
 from lp.app.errors import NotFoundError
@@ -56,8 +57,8 @@ class LinkCheckerAPI(LaunchpadView):
         result = {}
         links_to_check_data = self.request.get("link_hrefs")
         if links_to_check_data is None:
-            return simplejson.dumps(result)
-        links_to_check = simplejson.loads(links_to_check_data)
+            return json.dumps(result)
+        links_to_check = json.loads(links_to_check_data)
 
         for link_type in links_to_check:
             links = links_to_check[link_type]
@@ -65,7 +66,7 @@ class LinkCheckerAPI(LaunchpadView):
             result[link_type] = link_info
 
         self.request.response.setHeader("Content-type", "application/json")
-        return simplejson.dumps(result)
+        return json.dumps(result)
 
     def check_branch_links(self, links):
         """Check links of the form /+code/foo/bar"""
diff --git a/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py b/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
index 2e0af72..3222fa4 100644
--- a/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
+++ b/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
@@ -3,7 +3,8 @@
 
 """Tests for the InlineMultiCheckboxWidget."""
 
-import simplejson
+import json
+
 from lazr.enum import EnumeratedType, Item
 from zope.interface import Interface
 from zope.schema import List
@@ -58,18 +59,18 @@ class TestInlineMultiCheckboxWidget(TestCaseWithFactory):
             item.value, force_local_path=True
         )
         expected_items = self._makeExpectedItems(vocab, value_fn=value_fn)
-        self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
+        self.assertEqual(json.dumps(expected_items), widget.json_items)
 
     def test_items_for_custom_vocabulary(self):
         widget = self._getWidget(vocabulary=Alphabet)
         expected_items = self._makeExpectedItems(Alphabet)
-        self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
+        self.assertEqual(json.dumps(expected_items), widget.json_items)
 
     def test_items_for_custom_vocabulary_name(self):
         widget = self._getWidget(vocabulary="CountryName")
         vocab = getVocabularyRegistry().get(None, "CountryName")
         expected_items = self._makeExpectedItems(vocab)
-        self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
+        self.assertEqual(json.dumps(expected_items), widget.json_items)
 
     def test_selected_items_checked(self):
         widget = self._getWidget(
@@ -78,4 +79,4 @@ class TestInlineMultiCheckboxWidget(TestCaseWithFactory):
         expected_items = self._makeExpectedItems(
             Alphabet, selected=[Alphabet.A]
         )
-        self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
+        self.assertEqual(json.dumps(expected_items), widget.json_items)
diff --git a/lib/lp/app/browser/tests/test_launchpadform.py b/lib/lp/app/browser/tests/test_launchpadform.py
index 56f7f91..7ca3863 100644
--- a/lib/lp/app/browser/tests/test_launchpadform.py
+++ b/lib/lp/app/browser/tests/test_launchpadform.py
@@ -3,9 +3,9 @@
 
 """Tests for the lp.app.browser.launchpadform module."""
 
+import json
 from os.path import dirname, join
 
-import simplejson
 from lxml import html
 from testtools.content import text_content
 from zope.browserpage import ViewPageTemplateFile
@@ -246,7 +246,7 @@ class TestAjaxValidator(TestCase):
                 "errors": {"field.single_line": "An error occurred"},
                 "form_wide_errors": ["A form error"],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
     def test_non_ajax_failure_handler(self):
@@ -293,5 +293,5 @@ class TestAjaxValidator(TestCase):
                 "errors": {},
                 "form_wide_errors": ["An action error"],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
diff --git a/lib/lp/app/browser/tests/test_linkchecker.py b/lib/lp/app/browser/tests/test_linkchecker.py
index 9393a2e..0215c41 100644
--- a/lib/lp/app/browser/tests/test_linkchecker.py
+++ b/lib/lp/app/browser/tests/test_linkchecker.py
@@ -3,9 +3,9 @@
 
 """Unit tests for the LinkCheckerAPI."""
 
+import json
 from random import shuffle
 
-import simplejson
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.browser.linkchecker import LinkCheckerAPI
@@ -22,13 +22,13 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
     BRANCH_URL_TEMPLATE = "/+code/%s"
 
     def check_invalid_links(self, result_json, link_type, invalid_links):
-        link_dict = simplejson.loads(result_json)
+        link_dict = json.loads(result_json)
         links_to_check = link_dict[link_type]["invalid"]
         self.assertEqual(len(invalid_links), len(links_to_check))
         self.assertEqual(set(invalid_links), set(links_to_check))
 
     def check_valid_links(self, result_json, link_type, valid_links):
-        link_dict = simplejson.loads(result_json)
+        link_dict = json.loads(result_json)
         links_to_check = link_dict[link_type]["valid"]
         self.assertEqual(len(valid_links), len(links_to_check))
         self.assertEqual(set(valid_links), set(links_to_check))
@@ -109,7 +109,7 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
         shuffle(bug_urls)
 
         links_to_check = dict(branch_links=branch_urls, bug_links=bug_urls)
-        link_json = simplejson.dumps(links_to_check)
+        link_json = json.dumps(links_to_check)
 
         request = LaunchpadTestRequest(link_hrefs=link_json)
         link_checker = LinkCheckerAPI(object(), request)
@@ -124,7 +124,7 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
         request = LaunchpadTestRequest()
         link_checker = LinkCheckerAPI(object(), request)
         result_json = link_checker()
-        link_dict = simplejson.loads(result_json)
+        link_dict = json.loads(result_json)
         self.assertEqual(link_dict, {})
 
     def test_only_valid_links(self):
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 35676e0..4b81a7e 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -3,12 +3,12 @@
 
 """Test vocabulary adapters."""
 
+import json
 from datetime import datetime
 from typing import List
 from urllib.parse import urlencode
 
 import pytz
-import simplejson
 from zope.component import getSiteManager, getUtility
 from zope.formlib.interfaces import MissingInputError
 from zope.interface import implementer
@@ -609,7 +609,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
         bugtask = self.factory.makeBugTask(target=product)
         form = dict(name="TestPerson", search_text="xpting")
         view = self.create_vocabulary_view(form, context=bugtask)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         expected = [
             {
                 "alt_title": team.name,
@@ -669,7 +669,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
             name="TestPerson", search_text="xpting", search_filter=vocab_filter
         )
         view = self.create_vocabulary_view(form, context=product)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         entries = result["entries"]
         self.assertEqual(1, len(entries))
         self.assertEqual("xpting-person", entries[0]["value"])
@@ -684,7 +684,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
         login_person(person)
         form = dict(name="TestPerson", search_text="pting-n")
         view = self.create_vocabulary_view(form)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         expected = email[: MAX_DESCRIPTION_LENGTH - 3] + "..."
         self.assertEqual("pting-n", result["entries"][0]["value"])
         self.assertEqual(expected, result["entries"][0]["description"])
@@ -693,7 +693,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
         # The results are batched.
         form = dict(name="TestPerson", search_text="pting")
         view = self.create_vocabulary_view(form)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         total_size = result["total_size"]
         entries = len(result["entries"])
         self.assertTrue(
@@ -707,7 +707,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
             name="TestPerson", search_text="pting", start="0", batch="1"
         )
         view = self.create_vocabulary_view(form)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         self.assertEqual(6, result["total_size"])
         self.assertEqual(1, len(result["entries"]))
 
@@ -717,7 +717,7 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
             name="TestPerson", search_text="pting", start="1", batch="1"
         )
         view = self.create_vocabulary_view(form)
-        result = simplejson.loads(view())
+        result = json.loads(view())
         self.assertEqual(6, result["total_size"])
         self.assertEqual(1, len(result["entries"]))
         self.assertEqual("pting-2", result["entries"][0]["value"])
diff --git a/lib/lp/app/browser/vocabulary.py b/lib/lp/app/browser/vocabulary.py
index 126881c..04d0599 100644
--- a/lib/lp/app/browser/vocabulary.py
+++ b/lib/lp/app/browser/vocabulary.py
@@ -10,10 +10,11 @@ __all__ = [
     "vocabulary_filters",
 ]
 
+import json
+
 # This registers the registry.
 import zope.vocabularyregistry.registry  # noqa: F401  # isort: split
 
-import simplejson
 from lazr.restful.interfaces import IWebServiceClientRequest
 from zope.component import adapter, getUtility
 from zope.formlib.interfaces import MissingInputError
@@ -533,7 +534,7 @@ class HugeVocabularyJSONView:
             result.append(entry)
 
         self.request.response.setHeader("Content-type", "application/json")
-        return simplejson.dumps(dict(total_size=total_size, entries=result))
+        return json.dumps(dict(total_size=total_size, entries=result))
 
 
 def vocabulary_filters(vocabulary):
diff --git a/lib/lp/app/widgets/popup.py b/lib/lp/app/widgets/popup.py
index 3b879ee..b2524ef 100644
--- a/lib/lp/app/widgets/popup.py
+++ b/lib/lp/app/widgets/popup.py
@@ -13,7 +13,8 @@ __all__ = [
     "VocabularyPickerWidget",
 ]
 
-import simplejson
+import json
+
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.formlib.interfaces import ConversionError
@@ -160,7 +161,7 @@ class VocabularyPickerWidget(SingleDataHelper, ItemsWidgetBase):
 
     @property
     def json_config(self):
-        return simplejson.dumps(self.config)
+        return json.dumps(self.config)
 
     @property
     def extra_no_results_message(self):
diff --git a/lib/lp/app/widgets/tests/test_popup.py b/lib/lp/app/widgets/tests/test_popup.py
index 51b7162..0a4ce99 100644
--- a/lib/lp/app/widgets/tests/test_popup.py
+++ b/lib/lp/app/widgets/tests/test_popup.py
@@ -1,7 +1,8 @@
 # Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-import simplejson
+import json
+
 from zope.interface import Interface
 from zope.interface.interface import InterfaceClass
 from zope.schema import Choice
@@ -65,7 +66,7 @@ class TestVocabularyPickerWidget(TestCaseWithFactory):
             bound_field, self.vocabulary, self.request
         )
 
-        widget_config = simplejson.loads(picker_widget.json_config)
+        widget_config = json.loads(picker_widget.json_config)
         self.assertEqual("ValidTeamOwner", picker_widget.vocabulary_name)
         self.assertEqual(
             [
@@ -112,7 +113,7 @@ class TestVocabularyPickerWidget(TestCaseWithFactory):
             bound_field, vocabulary, self.request
         )
 
-        widget_config = simplejson.loads(picker_widget.json_config)
+        widget_config = json.loads(picker_widget.json_config)
         self.assertEqual(
             [
                 {
diff --git a/lib/lp/blueprints/browser/specificationsubscription.py b/lib/lp/blueprints/browser/specificationsubscription.py
index bb93933..c4e15ae 100644
--- a/lib/lp/blueprints/browser/specificationsubscription.py
+++ b/lib/lp/blueprints/browser/specificationsubscription.py
@@ -9,10 +9,10 @@ __all__ = [
     "SpecificationSubscriptionEditView",
 ]
 
+import json
 from typing import List
 
 from lazr.delegates import delegate_to
-from simplejson import dumps
 from zope.component import getUtility
 
 from lp import _
@@ -211,7 +211,7 @@ class SpecificationPortletSubcribersIds(LaunchpadView):
     @property
     def subscriber_ids_js(self):
         """Return subscriber_ids in a form suitable for JavaScript use."""
-        return dumps(self.subscriber_ids)
+        return json.dumps(self.subscriber_ids)
 
     def render(self):
         """Override the default render() to return only JSON."""
diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py
index 642b14b..eaf3ea6 100644
--- a/lib/lp/bugs/browser/bugsubscription.py
+++ b/lib/lp/bugs/browser/bugsubscription.py
@@ -11,11 +11,11 @@ __all__ = [
     "BugSubscriptionListView",
 ]
 
+import json
 from typing import List
 
 from lazr.delegates import delegate_to
 from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
-from simplejson import dumps
 from zope import formlib
 from zope.formlib.itemswidgets import RadioWidget
 from zope.formlib.widget import CustomWidgetFactory
@@ -663,7 +663,7 @@ class BugPortletSubscribersWithDetails(LaunchpadView):
 
     @property
     def subscriber_data_js(self):
-        return dumps(self.subscriber_data)
+        return json.dumps(self.subscriber_data)
 
     def render(self):
         """Override the default render() to return only JSON."""
diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
index e7b3dc6..7c5e70a 100644
--- a/lib/lp/bugs/browser/bugtarget.py
+++ b/lib/lp/bugs/browser/bugtarget.py
@@ -18,6 +18,7 @@ __all__ = [
 ]
 
 import http.client
+import json
 from datetime import datetime
 from functools import partial
 from io import BytesIO
@@ -26,7 +27,6 @@ from urllib.parse import quote, urlencode
 from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache
 from pytz import timezone
-from simplejson import dumps
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.formlib.form import Fields
@@ -1361,8 +1361,8 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
     @property
     def tags_js_data(self):
         """Return the JSON representation of the bug tags."""
-        # The model returns dict and list respectively but dumps blows up on
-        # security proxied objects.
+        # The model returns dict and list respectively, but json.dumps blows
+        # up on security proxied objects.
         used_tags = removeSecurityProxy(
             self.context.getUsedBugTagsWithOpenCounts(self.user)
         )
@@ -1373,9 +1373,9 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
                       var valid_name_pattern = %s;
                   </script>
                """ % (
-            dumps(used_tags),
-            dumps(official_tags),
-            dumps(valid_name_pattern.pattern),
+            json.dumps(used_tags),
+            json.dumps(official_tags),
+            json.dumps(valid_name_pattern.pattern),
         )
 
     @property
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index d000562..161e405 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -25,6 +25,7 @@ __all__ = [
     "get_visible_comments",
 ]
 
+import json
 import re
 from collections import defaultdict
 from datetime import datetime, timedelta
@@ -46,7 +47,6 @@ from lazr.restful.interfaces import (
 )
 from lazr.restful.utils import smartquote
 from pytz import utc
-from simplejson import dumps
 from zope import formlib
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import adapter, getAdapter, getMultiAdapter, getUtility
@@ -962,7 +962,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
         # Unwrap the security proxy. - official_tags is a security proxy
         # wrapped list.
         available_tags = list(self.context.bug.official_tags)
-        return "var available_official_tags = %s;" % dumps(available_tags)
+        return "var available_official_tags = %s;" % json.dumps(available_tags)
 
     @property
     def user_is_admin(self):
@@ -1775,7 +1775,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
                 self.request.response.setHeader(
                     "Content-type", "application/json"
                 )
-                return dumps(None)
+                return json.dumps(None)
             launchbag = getUtility(ILaunchBag)
             launchbag.add(bug.default_bugtask)
             # If we are deleting the current highlighted bugtask via ajax,
@@ -1788,7 +1788,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
                 self.request.response.setHeader(
                     "Content-type", "application/json"
                 )
-                return dumps(dict(bugtask_url=next_url))
+                return json.dumps(dict(bugtask_url=next_url))
             # No redirect required so return the new bugtask table HTML.
             view = getMultiAdapter(
                 (bug, self.request), name="+bugtasks-and-nominations-table"
diff --git a/lib/lp/bugs/browser/tests/test_bug_views.py b/lib/lp/bugs/browser/tests/test_bug_views.py
index 2264af4..927c93c 100644
--- a/lib/lp/bugs/browser/tests/test_bug_views.py
+++ b/lib/lp/bugs/browser/tests/test_bug_views.py
@@ -3,11 +3,11 @@
 
 """Tests for Bug Views."""
 
+import json
 import re
 from datetime import datetime, timedelta
 
 import pytz
-import simplejson
 from soupmatchers import HTMLContains, Tag, Within
 from storm.store import Store
 from testtools.matchers import Contains, Equals, MatchesAll, Not
@@ -484,7 +484,7 @@ class TestBugSecrecyViews(TestCaseWithFactory):
             **extra,
         )
         view = self.createInitializedSecrecyView(person, bug, request)
-        result_data = simplejson.loads(view.render())
+        result_data = json.loads(view.render())
 
         cache_data = result_data["cache_data"]
         self.assertFalse(cache_data["other_subscription_notifications"])
diff --git a/lib/lp/bugs/browser/tests/test_bugtask.py b/lib/lp/bugs/browser/tests/test_bugtask.py
index b816cdf..724af22 100644
--- a/lib/lp/bugs/browser/tests/test_bugtask.py
+++ b/lib/lp/bugs/browser/tests/test_bugtask.py
@@ -1,11 +1,11 @@
 # Copyright 2009-2022 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+import json
 import re
 from datetime import datetime, timedelta
 from urllib.parse import urlencode
 
-import simplejson
 import soupmatchers
 import transaction
 from lazr.restful.interfaces import IJSONRequestCache
@@ -1166,9 +1166,9 @@ class TestBugTaskDeleteView(TestCaseWithFactory):
             principal=bugtask.owner,
             **extra,
         )
-        result_data = simplejson.loads(view.render())
+        result_data = json.loads(view.render())
         self.assertEqual([bug.default_bugtask], bug.bugtasks)
-        notifications = simplejson.loads(
+        notifications = json.loads(
             view.request.response.getHeader("X-Lazr-Notifications")
         )
         self.assertEqual(1, len(notifications))
@@ -1198,9 +1198,9 @@ class TestBugTaskDeleteView(TestCaseWithFactory):
             principal=bug.owner,
             **extra,
         )
-        result_data = simplejson.loads(view.render())
+        result_data = json.loads(view.render())
         self.assertEqual([bug.default_bugtask], bug.bugtasks)
-        notifications = simplejson.loads(
+        notifications = json.loads(
             view.request.response.getHeader("X-Lazr-Notifications")
         )
         self.assertEqual(1, len(notifications))
diff --git a/lib/lp/bugs/browser/widgets/bug.py b/lib/lp/bugs/browser/widgets/bug.py
index 08dad8e..5c327b1 100644
--- a/lib/lp/bugs/browser/widgets/bug.py
+++ b/lib/lp/bugs/browser/widgets/bug.py
@@ -8,9 +8,9 @@ __all__ = [
     "LargeBugTagsWidget",
 ]
 
+import json
 import re
 
-from simplejson import dumps
 from zope.component import getUtility
 from zope.formlib.interfaces import ConversionError, WidgetInputError
 from zope.formlib.textwidgets import IntWidget, TextAreaWidget, TextWidget
@@ -168,7 +168,7 @@ class BugTagsWidget(BugTagsWidgetBase, TextWidget):
             official_tags = list(pillar_target.official_bug_tags)
         else:
             official_tags = []
-        return "var official_tags = %s;" % dumps(official_tags)
+        return "var official_tags = %s;" % json.dumps(official_tags)
 
 
 class BugTagsFrozenSetWidget(BugTagsWidget):
diff --git a/lib/lp/bugs/model/apportjob.py b/lib/lp/bugs/model/apportjob.py
index caf0fbd..a4612d5 100644
--- a/lib/lp/bugs/model/apportjob.py
+++ b/lib/lp/bugs/model/apportjob.py
@@ -8,9 +8,9 @@ __all__ = [
     "ApportJobDerived",
 ]
 
+import json
 from io import BytesIO
 
-import simplejson
 import six
 from lazr.delegates import delegate_to
 from storm.expr import And
@@ -60,10 +60,10 @@ class ApportJob(StormBase):
     # only delegates to ApportJob we can't simply directly access the
     # _json_data property, so we use a getter and setter here instead.
     def _set_metadata(self, metadata):
-        self._json_data = six.ensure_text(simplejson.dumps(metadata, "utf-8"))
+        self._json_data = six.ensure_text(json.dumps(metadata, "utf-8"))
 
     def _get_metadata(self):
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     metadata = property(_get_metadata, _set_metadata)
 
@@ -76,7 +76,7 @@ class ApportJob(StormBase):
             dict.
         """
         super().__init__()
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         self.job = Job()
         self.blob = blob
         self.job_type = job_type
diff --git a/lib/lp/bugs/stories/webservice/xx-bug-target.rst b/lib/lp/bugs/stories/webservice/xx-bug-target.rst
index f1fa5f2..8fb47c7 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug-target.rst
+++ b/lib/lp/bugs/stories/webservice/xx-bug-target.rst
@@ -16,12 +16,12 @@ All bug targets have a read/write bug_reporting_guidelines property.
     >>> print(product["bug_reporting_guidelines"])
     None
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> patch = {
     ...     "bug_reporting_guidelines": "Please run `ubuntu-bug -p firefox`."
     ... }
     >>> response = webservice.patch(
-    ...     product["self_link"], "application/json", dumps(patch)
+    ...     product["self_link"], "application/json", json.dumps(patch)
     ... )
 
     >>> product = webservice.get(product_url).jsonBody()
@@ -36,7 +36,7 @@ Not everyone can modify it however:
     ...     )
     ... }
     >>> response = user_webservice.patch(
-    ...     product["self_link"], "application/json", dumps(patch)
+    ...     product["self_link"], "application/json", json.dumps(patch)
     ... )
     >>> print(response)
     HTTP/1.1 401 Unauthorized...
@@ -109,7 +109,7 @@ The bug supervisor of a product can also add tags.
     ...     webservice.patch(
     ...         "/tags-test-product2",
     ...         "application/json",
-    ...         dumps({"bug_supervisor_link": ws_salgado["self_link"]}),
+    ...         json.dumps({"bug_supervisor_link": ws_salgado["self_link"]}),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -144,7 +144,6 @@ Official tags must conform to the same format as ordinary tags.
 
 We can also access official tags as a list.
 
-    >>> from simplejson import dumps
     >>> tags_test_product = webservice.get("/tags-test-product").jsonBody()
     >>> tags_test_product["official_bug_tags"]
     []
@@ -152,7 +151,7 @@ We can also access official tags as a list.
     ...     webservice.patch(
     ...         "/tags-test-product",
     ...         "application/json",
-    ...         dumps({"official_bug_tags": ["foo", "bar"]}),
+    ...         json.dumps({"official_bug_tags": ["foo", "bar"]}),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -171,7 +170,7 @@ We can also access official tags as a list.
     ...     webservice.patch(
     ...         "/testix",
     ...         "application/json",
-    ...         dumps({"official_bug_tags": ["foo", "bar"]}),
+    ...         json.dumps({"official_bug_tags": ["foo", "bar"]}),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -189,7 +188,9 @@ We can retrieve or set a person or team as the bug supervisor for projects.
     ...     webservice.patch(
     ...         "/firefox",
     ...         "application/json",
-    ...         dumps({"bug_supervisor_link": firefox_project["owner_link"]}),
+    ...         json.dumps(
+    ...             {"bug_supervisor_link": firefox_project["owner_link"]}
+    ...         ),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -208,7 +209,9 @@ We can also do this for distributions.
     ...     webservice.patch(
     ...         "/ubuntutest",
     ...         "application/json",
-    ...         dumps({"bug_supervisor_link": ubuntutest_dist["owner_link"]}),
+    ...         json.dumps(
+    ...             {"bug_supervisor_link": ubuntutest_dist["owner_link"]}
+    ...         ),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -223,7 +226,7 @@ Setting the bug supervisor is restricted to owners and launchpad admins.
     ...     user_webservice.patch(
     ...         "/ubuntutest",
     ...         "application/json",
-    ...         dumps({"bug_supervisor_link": None}),
+    ...         json.dumps({"bug_supervisor_link": None}),
     ...     )
     ... )
     HTTP/1.1 401 Unauthorized
diff --git a/lib/lp/bugs/stories/webservice/xx-bug.rst b/lib/lp/bugs/stories/webservice/xx-bug.rst
index 80c2f24..851aad5 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug.rst
+++ b/lib/lp/bugs/stories/webservice/xx-bug.rst
@@ -195,7 +195,7 @@ distribution in which the bug exists.
 
 To mark a bug as private, we patch the `private` attribute of the bug.
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> bug_twelve = webservice.get("/bugs/12").jsonBody()
     >>> bug_twelve["private"]
     False
@@ -203,7 +203,7 @@ To mark a bug as private, we patch the `private` attribute of the bug.
     ...     webservice.patch(
     ...         bug_twelve["self_link"],
     ...         "application/json",
-    ...         dumps(dict(private=True)),
+    ...         json.dumps(dict(private=True)),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -214,7 +214,7 @@ To mark a bug as private, we patch the `private` attribute of the bug.
     ...     webservice.patch(
     ...         bug_twelve["self_link"],
     ...         "application/json",
-    ...         dumps(dict(private=False)),
+    ...         json.dumps(dict(private=False)),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -228,7 +228,7 @@ attribute of the bug.
     ...     webservice.patch(
     ...         bug_twelve["self_link"],
     ...         "application/json",
-    ...         dumps(dict(duplicate_of_link=bug_one["self_link"])),
+    ...         json.dumps(dict(duplicate_of_link=bug_one["self_link"])),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -242,7 +242,7 @@ Now set it back to none:
     ...     webservice.patch(
     ...         bug_twelve["self_link"],
     ...         "application/json",
-    ...         dumps(dict(duplicate_of_link=None)),
+    ...         json.dumps(dict(duplicate_of_link=None)),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -259,7 +259,7 @@ Due to bug #1088358 the error is escaped as if it was HTML.
     ...     webservice.patch(
     ...         dupe_url,
     ...         "application/json",
-    ...         dumps(
+    ...         json.dumps(
     ...             dict(
     ...                 duplicate_of_link=webservice.getAbsoluteUrl("/bugs/5")
     ...             )
@@ -272,7 +272,7 @@ Due to bug #1088358 the error is escaped as if it was HTML.
     ...     webservice.patch(
     ...         webservice.getAbsoluteUrl("/bugs/5"),
     ...         "application/json",
-    ...         dumps(dict(duplicate_of_link=dupe_url)),
+    ...         json.dumps(dict(duplicate_of_link=dupe_url)),
     ...     )
     ... )
     HTTP/1.1 400 Bad Request
@@ -285,7 +285,7 @@ Due to bug #1088358 the error is escaped as if it was HTML.
     ...     webservice.patch(
     ...         dupe_url,
     ...         "application/json",
-    ...         dumps(dict(duplicate_of_link=None)),
+    ...         json.dumps(dict(duplicate_of_link=None)),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
@@ -444,7 +444,9 @@ It's possible to change the task's assignee.
     >>> patch = {"assignee_link": webservice.getAbsoluteUrl("/~cprov")}
     >>> bugtask_path = bug_one_bugtasks[0]["self_link"]
     >>> print(
-    ...     webservice.patch(bugtask_path, "application/json", dumps(patch))
+    ...     webservice.patch(
+    ...         bugtask_path, "application/json", json.dumps(patch)
+    ...     )
     ... )
     HTTP/1.1 209 Content Returned...
 
@@ -460,7 +462,9 @@ The task's importance can be modified directly.
 
     >>> patch = {"importance": "High"}
     >>> print(
-    ...     webservice.patch(bugtask_path, "application/json", dumps(patch))
+    ...     webservice.patch(
+    ...         bugtask_path, "application/json", json.dumps(patch)
+    ...     )
     ... )
     HTTP/1.1 209 Content Returned...
 
@@ -489,7 +493,9 @@ The task's status can also be modified directly.
 
     >>> patch = {"status": "Fix Committed"}
     >>> print(
-    ...     webservice.patch(bugtask_path, "application/json", dumps(patch))
+    ...     webservice.patch(
+    ...         bugtask_path, "application/json", json.dumps(patch)
+    ...     )
     ... )
     HTTP/1.1 209 Content Returned...
 
@@ -507,7 +513,9 @@ If an error occurs during a request that sets both 'status' and
 
     >>> patch = {"importance": "High", "status": "No Such Status"}
     >>> print(
-    ...     webservice.patch(bugtask_path, "application/json", dumps(patch))
+    ...     webservice.patch(
+    ...         bugtask_path, "application/json", json.dumps(patch)
+    ...     )
     ... )
     HTTP/1.1 400 Bad Request...
 
@@ -528,7 +536,9 @@ The milestone can only be set by appropriately privileged users.
     ...     )
     ... }
     >>> print(
-    ...     webservice.patch(bugtask_path, "application/json", dumps(patch))
+    ...     webservice.patch(
+    ...         bugtask_path, "application/json", json.dumps(patch)
+    ...     )
     ... )
     HTTP/1.1 209 Content Returned...
 
@@ -546,7 +556,7 @@ unchanged value.
     ... }
     >>> print(
     ...     user_webservice.patch(
-    ...         bugtask_path, "application/json", dumps(patch)
+    ...         bugtask_path, "application/json", json.dumps(patch)
     ...     )
     ... )
     HTTP/1.1 401 Unauthorized...
@@ -583,7 +593,7 @@ We can also PATCH the target attribute to accomplish the same thing.
     ...     webservice.patch(
     ...         task["self_link"].replace("mozilla-firefox", "evolution"),
     ...         "application/json",
-    ...         dumps(
+    ...         json.dumps(
     ...             {
     ...                 "target_link": webservice.getAbsoluteUrl(
     ...                     "/debian/+source/alsa-utils"
@@ -636,7 +646,9 @@ target, we reset it in order to avoid data inconsistencies.
     ... }
     >>> print(
     ...     webservice.patch(
-    ...         firefox_bugtask["self_link"], "application/json", dumps(patch)
+    ...         firefox_bugtask["self_link"],
+    ...         "application/json",
+    ...         json.dumps(patch),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned
@@ -1104,7 +1116,7 @@ They can also update the subscription's bug_notification_level directly.
     ...     webservice.patch(
     ...         new_subscription["self_link"],
     ...         "application/json",
-    ...         dumps(patch),
+    ...         json.dumps(patch),
     ...         api_version="devel",
     ...     ).jsonBody()
     ... )
@@ -1347,7 +1359,7 @@ We can modify the remote bug.
 
     >>> patch = {"remote_bug": "1234"}
     >>> response = webservice.patch(
-    ...     bug_watch_2000["self_link"], "application/json", dumps(patch)
+    ...     bug_watch_2000["self_link"], "application/json", json.dumps(patch)
     ... )
 
     >>> bug_watch = webservice.get(bug_watch_2000["self_link"]).jsonBody()
@@ -1358,7 +1370,7 @@ But we can't change other things, like the URL.
 
     >>> patch = {"url": "http://www.example.com/"}
     >>> response = webservice.patch(
-    ...     bug_watch_2000["self_link"], "application/json", dumps(patch)
+    ...     bug_watch_2000["self_link"], "application/json", json.dumps(patch)
     ... )
     >>> print(response)
     HTTP/1.1 400 Bad Request...
@@ -1443,7 +1455,7 @@ We can change various aspects of bug trackers.
     ...     "contact_details": "bob@xxxxxxxxxxx",
     ... }
     >>> response = webservice.patch(
-    ...     bug_tracker["self_link"], "application/json", dumps(patch)
+    ...     bug_tracker["self_link"], "application/json", json.dumps(patch)
     ... )
     >>> print(response)
     HTTP/1.1 301 Moved Permanently...
@@ -1493,7 +1505,7 @@ Non-admins can't disable a bugtracker through the API.
     ...     public_webservice.patch(
     ...         bug_tracker_path,
     ...         "application/json",
-    ...         dumps(dict(active=False)),
+    ...         json.dumps(dict(active=False)),
     ...     )
     ... )
     HTTP/1.1 401 Unauthorized
@@ -1503,7 +1515,9 @@ Non-admins can't disable a bugtracker through the API.
 Admins can, however.
 
     >>> bug_tracker = webservice.patch(
-    ...     bug_tracker_path, "application/json", dumps(dict(active=False))
+    ...     bug_tracker_path,
+    ...     "application/json",
+    ...     json.dumps(dict(active=False)),
     ... ).jsonBody()
     >>> pprint_entry(bug_tracker)
     active: False...
diff --git a/lib/lp/bugs/tests/bug.py b/lib/lp/bugs/tests/bug.py
index acfa1a2..3cc9b4b 100644
--- a/lib/lp/bugs/tests/bug.py
+++ b/lib/lp/bugs/tests/bug.py
@@ -3,6 +3,7 @@
 
 """Helper functions for bug-related doctests and pagetests."""
 
+import json
 import re
 import textwrap
 from datetime import datetime, timedelta
@@ -40,9 +41,7 @@ def print_also_notified(bug_page):
 
 def print_subscribers(bug_page, subscription_level=None, reverse=False):
     """Print the subscribers listed in the subscribers JSON portlet."""
-    from simplejson import loads
-
-    details = loads(bug_page)
+    details = json.loads(bug_page.decode())
 
     if details is None:
         # No subscribers at all.
diff --git a/lib/lp/bugs/tests/test_bugs_webservice.py b/lib/lp/bugs/tests/test_bugs_webservice.py
index 92664a4..fcd29c0 100644
--- a/lib/lp/bugs/tests/test_bugs_webservice.py
+++ b/lib/lp/bugs/tests/test_bugs_webservice.py
@@ -11,7 +11,6 @@ from datetime import datetime, timedelta
 import pytz
 import six
 from lazr.lifecycle.interfaces import IDoNotSnapshot
-from simplejson import dumps
 from storm.store import Store
 from testtools.matchers import Equals, LessThan
 from zope.component import getMultiAdapter
@@ -121,7 +120,7 @@ class TestBugDescriptionRepresentation(TestCaseWithFactory):
         response = self.webservice.patch(
             bug_two_json["self_link"],
             "application/json",
-            dumps(dict(description=new_description)),
+            json.dumps(dict(description=new_description)),
             headers=dict(accept="application/xhtml+xml"),
         )
 
@@ -517,7 +516,7 @@ class TestBugDateLastUpdated(TestCaseWithFactory):
         date_last_updated = bug.date_last_updated
         logout()
         response = webservice.patch(
-            task_url, "application/json", dumps(dict(status="Invalid"))
+            task_url, "application/json", json.dumps(dict(status="Invalid"))
         )
         self.assertEqual(209, response.status)
         with person_logged_in(owner):
diff --git a/lib/lp/code/browser/branch.py b/lib/lp/code/browser/branch.py
index 1ecf228..5ecce7e 100644
--- a/lib/lp/code/browser/branch.py
+++ b/lib/lp/code/browser/branch.py
@@ -22,10 +22,10 @@ __all__ = [
     "RegisterBranchMergeProposalView",
 ]
 
+import json
 from datetime import datetime
 
 import pytz
-import simplejson
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restful.fields import Reference
@@ -1354,7 +1354,7 @@ class RegisterBranchMergeProposalView(LaunchpadFormView):
         if self.request.is_ajax and len(visible_branches) < 2:
             self.request.response.setStatus(400, "Branch Visibility")
             self.request.response.setHeader("Content-Type", "application/json")
-            return simplejson.dumps(
+            return json.dumps(
                 {
                     "person_name": visibility_info["person_name"],
                     "branches_to_check": branch_names,
diff --git a/lib/lp/code/browser/branchmergeproposal.py b/lib/lp/code/browser/branchmergeproposal.py
index a2fe57e..a789afb 100644
--- a/lib/lp/code/browser/branchmergeproposal.py
+++ b/lib/lp/code/browser/branchmergeproposal.py
@@ -24,11 +24,11 @@ __all__ = [
     "latest_proposals_for_each_branch",
 ]
 
+import json
 import operator
 from functools import wraps
 from urllib.parse import urlsplit, urlunsplit
 
-import simplejson
 from lazr.delegates import delegate_to
 from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
@@ -851,7 +851,7 @@ class BranchMergeProposalView(
     @property
     def status_config(self):
         """The config to configure the ChoiceSource JS widget."""
-        return simplejson.dumps(
+        return json.dumps(
             {
                 "status_widget_items": vocabulary_to_choice_edit_items(
                     self._createStatusVocabulary(),
diff --git a/lib/lp/code/browser/sourcepackagerecipe.py b/lib/lp/code/browser/sourcepackagerecipe.py
index be599c4..002520d 100644
--- a/lib/lp/code/browser/sourcepackagerecipe.py
+++ b/lib/lp/code/browser/sourcepackagerecipe.py
@@ -14,8 +14,8 @@ __all__ = [
 ]
 
 import itertools
+import json
 
-import simplejson
 from brzbuildrecipe.recipe import ForbiddenInstructionError, RecipeParseError
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
@@ -485,7 +485,7 @@ class SourcePackageRecipeRequestBuildsAjaxView(
         return_data = dict(builds=builds, errors=errors)
         if informational:
             return_data.update(informational)
-        return simplejson.dumps(return_data)
+        return json.dumps(return_data)
 
     def failure(self, action, data, errors):
         """Called by the form if validate() finds any errors.
diff --git a/lib/lp/code/browser/tests/test_branchmergeproposal.py b/lib/lp/code/browser/tests/test_branchmergeproposal.py
index b481374..e624054 100644
--- a/lib/lp/code/browser/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/browser/tests/test_branchmergeproposal.py
@@ -5,12 +5,12 @@
 
 import doctest
 import hashlib
+import json
 import re
 from datetime import datetime, timedelta
 from difflib import diff_bytes, unified_diff
 
 import pytz
-import simplejson
 import transaction
 from fixtures import FakeLogger
 from lazr.lifecycle.event import ObjectModifiedEvent
@@ -877,7 +877,7 @@ class TestRegisterBranchMergeProposalViewBzr(
         self.assertEqual(
             "400 Branch Visibility", view.request.response.getStatusString()
         )
-        self.assertEqual(expected_data, simplejson.loads(result_data))
+        self.assertEqual(expected_data, json.loads(result_data))
 
     def test_register_ajax_request_with_validation_errors(self):
         # Ajax submits where there is a validation error in the submitted data
@@ -917,7 +917,7 @@ class TestRegisterBranchMergeProposalViewBzr(
                 },
                 "form_wide_errors": [],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
 
@@ -1013,7 +1013,7 @@ class TestRegisterBranchMergeProposalViewGit(
             "400 Repository Visibility",
             view.request.response.getStatusString(),
         )
-        self.assertEqual(expected_data, simplejson.loads(result_data))
+        self.assertEqual(expected_data, json.loads(result_data))
 
     def test_register_ajax_request_with_validation_errors(self):
         # Ajax submits where there is a validation error in the submitted data
@@ -1053,7 +1053,7 @@ class TestRegisterBranchMergeProposalViewGit(
                 },
                 "form_wide_errors": [],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
     def test_register_ajax_request_with_missing_target_git_repository(self):
@@ -1086,7 +1086,7 @@ class TestRegisterBranchMergeProposalViewGit(
                 },
                 "form_wide_errors": [],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
     def test_register_ajax_request_with_missing_target_git_path(self):
@@ -1123,7 +1123,7 @@ class TestRegisterBranchMergeProposalViewGit(
                 },
                 "form_wide_errors": [],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
     def test_register_ajax_request_with_missing_prerequisite_git_path(self):
@@ -1170,7 +1170,7 @@ class TestRegisterBranchMergeProposalViewGit(
                 },
                 "form_wide_errors": [],
             },
-            simplejson.loads(view.form_result),
+            json.loads(view.form_result),
         )
 
 
diff --git a/lib/lp/code/model/branchmergeproposaljob.py b/lib/lp/code/model/branchmergeproposaljob.py
index d120ebe..35c8bdf 100644
--- a/lib/lp/code/model/branchmergeproposaljob.py
+++ b/lib/lp/code/model/branchmergeproposaljob.py
@@ -19,11 +19,11 @@ __all__ = [
     "UpdatePreviewDiffJob",
 ]
 
+import json
 from contextlib import ExitStack
 from datetime import datetime, timedelta
 
 import pytz
-import simplejson
 import six
 from lazr.delegates import delegate_to
 from lazr.enum import DBEnumeratedType, DBItem
@@ -155,7 +155,7 @@ class BranchMergeProposalJob(StormBase):
 
     @property
     def metadata(self):
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     def __init__(self, branch_merge_proposal, job_type, metadata):
         """Constructor.
@@ -166,7 +166,7 @@ class BranchMergeProposalJob(StormBase):
             dict.
         """
         super().__init__()
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         self.job = Job()
         self.branch_merge_proposal = branch_merge_proposal
         self.job_type = job_type
diff --git a/lib/lp/code/model/diff.py b/lib/lp/code/model/diff.py
index 21e9968..7def5fd 100644
--- a/lib/lp/code/model/diff.py
+++ b/lib/lp/code/model/diff.py
@@ -10,12 +10,12 @@ __all__ = [
 ]
 
 import io
+import json
 import sys
 from contextlib import ExitStack
 from operator import attrgetter
 from uuid import uuid1
 
-import simplejson
 import six
 from breezy import trace
 from breezy.diff import show_diff_trees
@@ -60,7 +60,7 @@ class Diff(SQLBase):
             return None
         return {
             key: tuple(value)
-            for key, value in simplejson.loads(self._diffstat).items()
+            for key, value in json.loads(self._diffstat).items()
         }
 
     def _set_diffstat(self, diffstat):
@@ -69,7 +69,7 @@ class Diff(SQLBase):
             return
         # diffstats should be mappings of path to line counts.
         assert isinstance(diffstat, dict)
-        self._diffstat = simplejson.dumps(diffstat)
+        self._diffstat = json.dumps(diffstat)
 
     diffstat = property(_get_diffstat, _set_diffstat)
 
diff --git a/lib/lp/registry/browser/team.py b/lib/lp/registry/browser/team.py
index 32afa9a..8de222c 100644
--- a/lib/lp/registry/browser/team.py
+++ b/lib/lp/registry/browser/team.py
@@ -30,13 +30,12 @@ __all__ = [
     "TeamReassignmentView",
 ]
 
-
+import json
 import math
 from datetime import datetime, timedelta
 from urllib.parse import unquote
 
 import pytz
-import simplejson
 from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache
 from lazr.restful.utils import smartquote
@@ -1061,7 +1060,7 @@ class TeamMailingListArchiveView(LaunchpadView):
         # XXX: jcsackett 18-1-2012: This needs to be updated to use the
         # grackle client, once that is available, instead of returning
         # an empty list as it does now.
-        return simplejson.loads("[]")
+        return json.loads("[]")
 
 
 class TeamAddView(TeamFormMixin, HasRenewalPolicyMixin, LaunchpadFormView):
diff --git a/lib/lp/registry/browser/tests/test_pillar_sharing.py b/lib/lp/registry/browser/tests/test_pillar_sharing.py
index c295f6d..75abf61 100644
--- a/lib/lp/registry/browser/tests/test_pillar_sharing.py
+++ b/lib/lp/registry/browser/tests/test_pillar_sharing.py
@@ -3,7 +3,8 @@
 
 """Test views that manage sharing."""
 
-import simplejson
+import json
+
 from fixtures import FakeLogger
 from lazr.restful.interfaces import IJSONRequestCache
 from lazr.restful.utils import get_current_web_service_request
@@ -307,7 +308,7 @@ class PillarSharingViewTestMixin:
     def test_picker_config(self):
         # Test the config passed to the disclosure sharing picker.
         view = create_view(self.pillar, name="+sharing")
-        picker_config = simplejson.loads(view.json_sharing_picker_config)
+        picker_config = json.loads(view.json_sharing_picker_config)
         self.assertTrue("vocabulary_filters" in picker_config)
         self.assertEqual("Share project information", picker_config["header"])
         self.assertEqual(
diff --git a/lib/lp/registry/browser/tests/test_team.py b/lib/lp/registry/browser/tests/test_team.py
index fdbef5a..6330233 100644
--- a/lib/lp/registry/browser/tests/test_team.py
+++ b/lib/lp/registry/browser/tests/test_team.py
@@ -2,8 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 import contextlib
+import json
 
-import simplejson
 import soupmatchers
 import transaction
 from lazr.restful.interfaces import IJSONRequestCache
@@ -740,7 +740,7 @@ class TestMailingListArchiveView(TestCaseWithFactory):
     @contextlib.contextmanager
     def _override_messages(self, view_class, messages):
         def _message_shim(self):
-            return simplejson.loads(messages)
+            return json.loads(messages)
 
         tmp = TeamMailingListArchiveView._get_messages
         TeamMailingListArchiveView._get_messages = _message_shim
diff --git a/lib/lp/registry/model/persontransferjob.py b/lib/lp/registry/model/persontransferjob.py
index 14eaa9d..78bf6a9 100644
--- a/lib/lp/registry/model/persontransferjob.py
+++ b/lib/lp/registry/model/persontransferjob.py
@@ -9,10 +9,10 @@ __all__ = [
     "PersonTransferJob",
 ]
 
+import json
 from datetime import datetime
 
 import pytz
-import simplejson
 import six
 import transaction
 from lazr.delegates import delegate_to
@@ -84,7 +84,7 @@ class PersonTransferJob(StormBase):
 
     @property
     def metadata(self):
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     def __init__(
         self, minor_person, major_person, job_type, metadata, requester=None
@@ -105,7 +105,7 @@ class PersonTransferJob(StormBase):
         self.major_person = major_person
         self.minor_person = minor_person
 
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring,
         # but the DB representation is unicode.
         self._json_data = six.ensure_text(json_data)
diff --git a/lib/lp/registry/model/productjob.py b/lib/lp/registry/model/productjob.py
index 5ca685d..107ffac 100644
--- a/lib/lp/registry/model/productjob.py
+++ b/lib/lp/registry/model/productjob.py
@@ -11,9 +11,9 @@ __all__ = [
     "ThirtyDayCommercialExpirationJob",
 ]
 
+import json
 from datetime import datetime, timedelta
 
-import simplejson
 import six
 from lazr.delegates import delegate_to
 from pytz import utc
@@ -125,7 +125,7 @@ class ProductJob(StormBase):
 
     @property
     def metadata(self):
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     def __init__(self, product, job_type, metadata):
         """Constructor.
@@ -138,7 +138,7 @@ class ProductJob(StormBase):
         self.job = Job()
         self.product = product
         self.job_type = job_type
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         self._json_data = six.ensure_text(json_data)
 
 
diff --git a/lib/lp/registry/model/sharingjob.py b/lib/lp/registry/model/sharingjob.py
index bd9ba75..3dc412d 100644
--- a/lib/lp/registry/model/sharingjob.py
+++ b/lib/lp/registry/model/sharingjob.py
@@ -7,9 +7,9 @@ __all__ = [
     "RemoveArtifactSubscriptionsJob",
 ]
 
+import json
 import logging
 
-import simplejson
 import six
 from lazr.delegates import delegate_to
 from lazr.enum import DBEnumeratedType, DBItem
@@ -124,7 +124,7 @@ class SharingJob(StormBase):
 
     @property
     def metadata(self):
-        return simplejson.loads(self._json_data)
+        return json.loads(self._json_data)
 
     def __init__(self, job_type, pillar, grantee, metadata):
         """Constructor.
@@ -134,7 +134,7 @@ class SharingJob(StormBase):
             dict.
         """
         super().__init__()
-        json_data = simplejson.dumps(metadata)
+        json_data = json.dumps(metadata)
         self.job = Job()
         self.job_type = job_type
         self.grantee = grantee
diff --git a/lib/lp/registry/stories/webservice/xx-distribution-mirror.rst b/lib/lp/registry/stories/webservice/xx-distribution-mirror.rst
index 871d46b..057ac11 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution-mirror.rst
+++ b/lib/lp/registry/stories/webservice/xx-distribution-mirror.rst
@@ -81,11 +81,11 @@ Security checks
 People who are not mirror listing admins or the mirrors registrar may not
 change the owner's of mirrors:
 
+    >>> import json
     >>> from zope.component import getUtility
     >>> from lp.testing.pages import webservice_for_person
     >>> from lp.services.webapp.interfaces import OAuthPermission
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from simplejson import dumps
     >>> login(ANONYMOUS)
     >>> karl_db = getUtility(IPersonSet).getByName("karl")
     >>> test_db = getUtility(IPersonSet).getByName("name12")
@@ -187,7 +187,9 @@ authorized.
     >>> karl = webservice.get("/~karl").jsonBody()
     >>> patch = {"owner_link": karl["self_link"]}
     >>> response = test_webservice.patch(
-    ...     canonical_archive["self_link"], "application/json", dumps(patch)
+    ...     canonical_archive["self_link"],
+    ...     "application/json",
+    ...     json.dumps(patch),
     ... )
     >>> response.status
     401
@@ -196,7 +198,9 @@ But if we use Karl, the mirror listing admin's, webservice, we can update
 the owner.
 
     >>> response = karl_webservice.patch(
-    ...     canonical_archive["self_link"], "application/json", dumps(patch)
+    ...     canonical_archive["self_link"],
+    ...     "application/json",
+    ...     json.dumps(patch),
     ... )
     >>> response.status
     209
@@ -216,7 +220,9 @@ Some attributes are read-only via the API:
     ...     "reviewer_link": karl["self_link"],
     ... }
     >>> response = karl_webservice.patch(
-    ...     canonical_releases["self_link"], "application/json", dumps(patch)
+    ...     canonical_releases["self_link"],
+    ...     "application/json",
+    ...     json.dumps(patch),
     ... )
     >>> print(response)
     HTTP/1.1 400 Bad Request
@@ -237,13 +243,17 @@ While others can be set with the appropriate authorization:
     ...     "whiteboard": "This mirror is too shiny to be true",
     ... }
     >>> response = test_webservice.patch(
-    ...     canonical_releases["self_link"], "application/json", dumps(patch)
+    ...     canonical_releases["self_link"],
+    ...     "application/json",
+    ...     json.dumps(patch),
     ... )
     >>> response.status
     401
 
     >>> response = karl_webservice.patch(
-    ...     canonical_releases["self_link"], "application/json", dumps(patch)
+    ...     canonical_releases["self_link"],
+    ...     "application/json",
+    ...     json.dumps(patch),
     ... ).jsonBody()
     >>> pprint_entry(response)
     base_url: 'http://releases.ubuntu.com/'
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.rst b/lib/lp/registry/stories/webservice/xx-distribution.rst
index aa2b91e..79a1450 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.rst
+++ b/lib/lp/registry/stories/webservice/xx-distribution.rst
@@ -197,12 +197,12 @@ returning None if there isn't one.
 
 Prepare stuff.
 
+    >>> import json
     >>> from zope.component import getUtility
     >>> from lp.testing.pages import webservice_for_person
     >>> from lp.services.webapp.interfaces import OAuthPermission
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from simplejson import dumps
 
     >>> login("admin@xxxxxxxxxxxxx")
     >>> ubuntu_distro = getUtility(IDistributionSet).getByName("ubuntu")
@@ -235,7 +235,7 @@ Mark new mirror as official and a country mirror.
     >>> response = karl_webservice.patch(
     ...     antarctica_patch_target["self_link"],
     ...     "application/json",
-    ...     dumps(patch),
+    ...     json.dumps(patch),
     ... )
 
     >>> antarctica = webservice.get("/+countries/AQ").jsonBody()
diff --git a/lib/lp/registry/stories/webservice/xx-person.rst b/lib/lp/registry/stories/webservice/xx-person.rst
index 93bcbba..1249d3e 100644
--- a/lib/lp/registry/stories/webservice/xx-person.rst
+++ b/lib/lp/registry/stories/webservice/xx-person.rst
@@ -576,10 +576,10 @@ to, obviously.
 
 Wiki names can be modified.
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> patch = {"wiki": "http://www.example.com/";, "wikiname": "MrExample"}
     >>> response = webservice.patch(
-    ...     wiki_name["self_link"], "application/json", dumps(patch)
+    ...     wiki_name["self_link"], "application/json", json.dumps(patch)
     ... )
     >>> wiki_name = sorted(webservice.get(wikis_link).jsonBody()["entries"])[
     ...     0
@@ -592,7 +592,7 @@ escaped as if it was HTML.
 
     >>> patch = {"wiki": "javascript:void/**/", "wikiname": "MrExample"}
     >>> response = webservice.patch(
-    ...     wiki_name["self_link"], "application/json", dumps(patch)
+    ...     wiki_name["self_link"], "application/json", json.dumps(patch)
     ... )
     >>> print(response)
     HTTP/1.1 400 Bad Request
@@ -969,12 +969,9 @@ Restrictions
 
 A team can't be its own owner.
 
-    >>> import simplejson
     >>> doc = {"team_owner_link": webservice.getAbsoluteUrl("/~admins")}
     >>> print(
-    ...     webservice.patch(
-    ...         "/~admins", "application/json", simplejson.dumps(doc)
-    ...     )
+    ...     webservice.patch("/~admins", "application/json", json.dumps(doc))
     ... )
     HTTP/1.1 400 Bad Request
     ...
diff --git a/lib/lp/registry/stories/webservice/xx-private-team.rst b/lib/lp/registry/stories/webservice/xx-private-team.rst
index 43e1e16..b69f5c1 100644
--- a/lib/lp/registry/stories/webservice/xx-private-team.rst
+++ b/lib/lp/registry/stories/webservice/xx-private-team.rst
@@ -130,13 +130,11 @@ Create a webservice object for commercial-admins.
 A commercial admin may change the visibility.  There is no helper
 method to do it, but it can be changed via a patch.
 
-    >>> import simplejson
+    >>> import json
     >>> def modify_team(team, representation, method, service):
     ...     "A helper function to send a PUT or PATCH request to a team."
     ...     headers = {"Content-type": "application/json"}
-    ...     return service(
-    ...         team, method, simplejson.dumps(representation), headers
-    ...     )
+    ...     return service(team, method, json.dumps(representation), headers)
     ...
 
     >>> print(
diff --git a/lib/lp/registry/stories/webservice/xx-project-registry.rst b/lib/lp/registry/stories/webservice/xx-project-registry.rst
index 4ab95f4..796f357 100644
--- a/lib/lp/registry/stories/webservice/xx-project-registry.rst
+++ b/lib/lp/registry/stories/webservice/xx-project-registry.rst
@@ -371,7 +371,7 @@ development_focus_link.
 
 Attributes can be edited via the webservice.patch() method.
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> patch = {
     ...     "driver_link": webservice.getAbsoluteUrl("/~mark"),
     ...     "homepage_url": "http://sf.net/firefox";,
@@ -380,7 +380,11 @@ Attributes can be edited via the webservice.patch() method.
     ...         "/bugs/bugtrackers/mozilla.org"
     ...     ),
     ... }
-    >>> print(webservice.patch("/firefox", "application/json", dumps(patch)))
+    >>> print(
+    ...     webservice.patch(
+    ...         "/firefox", "application/json", json.dumps(patch)
+    ...     )
+    ... )
     HTTP/1.1 209 Content Returned
     ...
 
@@ -434,7 +438,7 @@ changed as well.
     ... }
     >>> print(
     ...     webservice.patch(
-    ...         "/test-project", "application/json", dumps(patch)
+    ...         "/test-project", "application/json", json.dumps(patch)
     ...     )
     ... )
     HTTP/1.1 209 Content Returned
@@ -450,7 +454,11 @@ webservice.patch() method.
     >>> patch = {
     ...     "registrant_link": webservice.getAbsoluteUrl("/~mark"),
     ... }
-    >>> print(webservice.patch("/firefox", "application/json", dumps(patch)))
+    >>> print(
+    ...     webservice.patch(
+    ...         "/firefox", "application/json", json.dumps(patch)
+    ...     )
+    ... )
     HTTP/1.1 400 Bad Request
     ...
     registrant_link: You tried to modify a read-only attribute.
@@ -463,7 +471,11 @@ Similarly the date_created attribute cannot be modified.
 
     >>> original_date_created = firefox["date_created"]
     >>> patch = {"date_created": "2000-01-01T01:01:01+00:00Z"}
-    >>> print(webservice.patch("/firefox", "application/json", dumps(patch)))
+    >>> print(
+    ...     webservice.patch(
+    ...         "/firefox", "application/json", json.dumps(patch)
+    ...     )
+    ... )
     HTTP/1.1 400 Bad Request
     ...
     date_created: You tried to modify a read-only attribute.
@@ -478,7 +490,7 @@ hierarchy of series, milestones, and releases.
     >>> patch = {"status": "Obsolete"}
     >>> print(
     ...     webservice.patch(
-    ...         "/firefox/trunk", "application/json", dumps(patch)
+    ...         "/firefox/trunk", "application/json", json.dumps(patch)
     ...     )
     ... )
     HTTP/1.1 209 Content Returned...
diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
index 348b7a7..3b974a8 100644
--- a/lib/lp/scripts/garbo.py
+++ b/lib/lp/scripts/garbo.py
@@ -11,6 +11,7 @@ __all__ = [
     "save_garbo_job_state",
 ]
 
+import json
 import logging
 import multiprocessing
 import os
@@ -20,7 +21,6 @@ from datetime import datetime, timedelta
 
 import iso8601
 import pytz
-import simplejson
 import six
 import transaction
 from contrib.glock import GlobalLock, LockAlreadyAcquired
@@ -168,14 +168,14 @@ def load_garbo_job_state(job_name):
         .get_one()
     )
     if job_data:
-        return simplejson.loads(job_data[0])
+        return json.loads(job_data[0])
     return None
 
 
 def save_garbo_job_state(job_name, job_data):
     # Save the json state data for the given job name.
     store = IMasterStore(Person)
-    json_data = simplejson.dumps(job_data, ensure_ascii=False)
+    json_data = json.dumps(job_data, ensure_ascii=False)
     result = store.execute(
         "UPDATE GarboJobState SET json_data = ? WHERE name = ?",
         params=(json_data, six.ensure_text(job_name)),
diff --git a/lib/lp/services/oauth/browser/__init__.py b/lib/lp/services/oauth/browser/__init__.py
index 0b70209..2a67f72 100644
--- a/lib/lp/services/oauth/browser/__init__.py
+++ b/lib/lp/services/oauth/browser/__init__.py
@@ -8,10 +8,10 @@ __all__ = [
     "lookup_oauth_context",
 ]
 
+import json
 from datetime import datetime, timedelta
 
 import pytz
-import simplejson
 from lazr.restful import HTTPResource
 from zope.component import getUtility
 from zope.formlib.form import Action, Actions, expandPrefix
@@ -58,7 +58,7 @@ class JSONTokenMixin:
         ]
         structure["access_levels"] = access_levels
         self.request.response.setHeader("Content-Type", HTTPResource.JSON_TYPE)
-        return simplejson.dumps(structure)
+        return json.dumps(structure)
 
 
 class OAuthRequestTokenView(LaunchpadFormView, JSONTokenMixin):
diff --git a/lib/lp/services/oauth/stories/authorize-token.rst b/lib/lp/services/oauth/stories/authorize-token.rst
index f71bd0b..39ce2bc 100644
--- a/lib/lp/services/oauth/stories/authorize-token.rst
+++ b/lib/lp/services/oauth/stories/authorize-token.rst
@@ -158,7 +158,7 @@ by the user is restricted to things related to that context.
 A client other than a web browser may request a JSON representation of
 the list of authentication levels.
 
-    >>> import simplejson
+    >>> import json
     >>> from lp.testing.pages import setupBrowser
 
     >>> json_browser = setupBrowser()
@@ -169,7 +169,7 @@ the list of authentication levels.
     >>> json_browser.open(
     ...     "http://launchpad.test/+authorize-token?%s"; % urlencode(params)
     ... )
-    >>> json_token = simplejson.loads(json_browser.contents)
+    >>> json_token = json.loads(json_browser.contents.decode())
     >>> sorted(json_token.keys())
     ['access_levels', 'oauth_token', 'oauth_token_consumer']
 
@@ -190,7 +190,7 @@ the list of authentication levels.
     ...     )
     ...     % urlencode(params)
     ... )
-    >>> json_token = simplejson.loads(json_browser.contents)
+    >>> json_token = json.loads(json_browser.contents.decode())
     >>> sorted(
     ...     (level["value"], level["title"])
     ...     for level in json_token["access_levels"]
diff --git a/lib/lp/services/oauth/stories/request-token.rst b/lib/lp/services/oauth/stories/request-token.rst
index 1496f13..2bc110a 100644
--- a/lib/lp/services/oauth/stories/request-token.rst
+++ b/lib/lp/services/oauth/stories/request-token.rst
@@ -22,7 +22,7 @@ The consumer can ask for a JSON representation of the request token,
 which will also include information about the available permission
 levels.
 
-    >>> import simplejson
+    >>> import json
     >>> from lp.testing.pages import setupBrowser
 
     >>> json_browser = setupBrowser()
@@ -30,7 +30,7 @@ levels.
     >>> json_browser.open(
     ...     "http://launchpad.test/+request-token";, data=urlencode(data)
     ... )
-    >>> token = simplejson.loads(json_browser.contents)
+    >>> token = json.loads(json_browser.contents.decode())
     >>> sorted(token.keys())
     ['access_levels', 'oauth_token', 'oauth_token_consumer',
      'oauth_token_secret']
diff --git a/lib/lp/services/webapp/batching.py b/lib/lp/services/webapp/batching.py
index 61fbc6f..35eb313 100644
--- a/lib/lp/services/webapp/batching.py
+++ b/lib/lp/services/webapp/batching.py
@@ -1,13 +1,13 @@
 # Copyright 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+import json
 import re
 from collections.abc import Sequence
 from datetime import datetime
 from functools import reduce
 
 import lazr.batchnavigator
-import simplejson
 from iso8601 import ParseError, parse_date
 from lazr.batchnavigator.interfaces import IRangeFactory
 from storm import Undef
@@ -194,7 +194,7 @@ class TableBatchNavigator(BatchNavigator):
                 self.show_column[column_to_show] = True
 
 
-class DateTimeJSONEncoder(simplejson.JSONEncoder):
+class DateTimeJSONEncoder(json.JSONEncoder):
     """A JSON encoder that understands datetime objects.
 
     Datetime objects are formatted according to ISO 1601.
@@ -203,7 +203,7 @@ class DateTimeJSONEncoder(simplejson.JSONEncoder):
     def default(self, obj):
         if isinstance(obj, datetime):
             return obj.isoformat()
-        return simplejson.JSONEncoder.default(self, obj)
+        return json.JSONEncoder.default(self, obj)
 
 
 class ShadowedList:
@@ -381,8 +381,8 @@ class StormRangeFactory:
         lower = self.getOrderValuesFor(plain_slice[0])
         upper = self.getOrderValuesFor(plain_slice[batch.trueSize - 1])
         return (
-            simplejson.dumps(lower, cls=DateTimeJSONEncoder),
-            simplejson.dumps(upper, cls=DateTimeJSONEncoder),
+            json.dumps(lower, cls=DateTimeJSONEncoder),
+            json.dumps(upper, cls=DateTimeJSONEncoder),
         )
 
     def reportError(self, message):
@@ -404,8 +404,8 @@ class StormRangeFactory:
         if memo == "":
             return None
         try:
-            parsed_memo = simplejson.loads(memo)
-        except simplejson.JSONDecodeError:
+            parsed_memo = json.loads(memo)
+        except json.JSONDecodeError:
             self.reportError("memo is not a valid JSON string.")
             return None
         if not isinstance(parsed_memo, list):
@@ -429,7 +429,7 @@ class StormRangeFactory:
             except TypeError as error:
                 # A TypeError is raised when the type of value cannot
                 # be used for expression. All expected types are
-                # properly created by simplejson.loads() above, except
+                # properly created by json.loads() above, except
                 # time stamps which are represented as strings in
                 # ISO format. If value is a string and if it can be
                 # converted into a datetime object, we have a valid
diff --git a/lib/lp/services/webapp/tests/test_batching.py b/lib/lp/services/webapp/tests/test_batching.py
index fdbf3f0..bc461db 100644
--- a/lib/lp/services/webapp/tests/test_batching.py
+++ b/lib/lp/services/webapp/tests/test_batching.py
@@ -1,10 +1,10 @@
 # Copyright 2011-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+import json
 from datetime import datetime
 
 import pytz
-import simplejson
 from lazr.batchnavigator.interfaces import IRangeFactory
 from storm.expr import Desc, compile
 from storm.store import EmptyResultSet
@@ -177,12 +177,12 @@ class TestStormRangeFactory(TestCaseWithFactory):
         # where the value is represented in the ISO time format.
         self.assertEqual(
             '"2011-07-25T00:00:00"',
-            simplejson.dumps(datetime(2011, 7, 25), cls=DateTimeJSONEncoder),
+            json.dumps(datetime(2011, 7, 25), cls=DateTimeJSONEncoder),
         )
 
         # DateTimeJSONEncoder works for the regular Python types that can
         # represented as JSON strings.
-        encoded = simplejson.dumps(
+        encoded = json.dumps(
             ("foo", 1, 2.0, [3, 4], {5: "bar"}, datetime(2011, 7, 24)),
             cls=DateTimeJSONEncoder,
         )
@@ -199,16 +199,16 @@ class TestStormRangeFactory(TestCaseWithFactory):
         range_factory = StormRangeFactory(resultset)
         memo_value = range_factory.getOrderValuesFor(resultset[0])
         request = LaunchpadTestRequest(
-            QUERY_STRING="memo=%s" % simplejson.dumps(memo_value)
+            QUERY_STRING="memo=%s" % json.dumps(memo_value)
         )
         batchnav = BatchNavigator(
             resultset, request, size=3, range_factory=range_factory
         )
         first, last = range_factory.getEndpointMemos(batchnav.batch)
-        expected_first = simplejson.dumps(
+        expected_first = json.dumps(
             [resultset[1].name], cls=DateTimeJSONEncoder
         )
-        expected_last = simplejson.dumps(
+        expected_last = json.dumps(
             [resultset[3].name], cls=DateTimeJSONEncoder
         )
         self.assertEqual(expected_first, first)
@@ -225,11 +225,11 @@ class TestStormRangeFactory(TestCaseWithFactory):
             resultset, request, size=3, range_factory=range_factory
         )
         first, last = range_factory.getEndpointMemos(batchnav.batch)
-        expected_first = simplejson.dumps(
+        expected_first = json.dumps(
             [resultset.get_plain_result_set()[0][1].id],
             cls=DateTimeJSONEncoder,
         )
-        expected_last = simplejson.dumps(
+        expected_last = json.dumps(
             [resultset.get_plain_result_set()[2][1].id],
             cls=DateTimeJSONEncoder,
         )
@@ -260,7 +260,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         # parseMemo() accepts only JSON representations of lists.
         resultset = self.makeStormResultSet()
         range_factory = StormRangeFactory(resultset, self.logError)
-        self.assertIs(None, range_factory.parseMemo(simplejson.dumps(1)))
+        self.assertIs(None, range_factory.parseMemo(json.dumps(1)))
         self.assertEqual(
             ["memo must be the JSON representation of a list."],
             self.error_messages,
@@ -273,7 +273,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset = self.makeStormResultSet()
         resultset.order_by(Person.name, Person.id)
         range_factory = StormRangeFactory(resultset, self.logError)
-        self.assertIs(None, range_factory.parseMemo(simplejson.dumps([1])))
+        self.assertIs(None, range_factory.parseMemo(json.dumps([1])))
         expected_message = (
             "Invalid number of elements in memo string. Expected: 2, got: 1"
         )
@@ -286,7 +286,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset.order_by(Person.datecreated, Person.name, Person.id)
         range_factory = StormRangeFactory(resultset, self.logError)
         invalid_memo = [datetime(2011, 7, 25, 11, 30, 30, 45), "foo", "bar"]
-        json_data = simplejson.dumps(invalid_memo, cls=DateTimeJSONEncoder)
+        json_data = json.dumps(invalid_memo, cls=DateTimeJSONEncoder)
         self.assertIs(None, range_factory.parseMemo(json_data))
         self.assertEqual(["Invalid parameter: 'bar'"], self.error_messages)
 
@@ -300,7 +300,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
             "foo",
             1,
         ]
-        json_data = simplejson.dumps(valid_memo, cls=DateTimeJSONEncoder)
+        json_data = json.dumps(valid_memo, cls=DateTimeJSONEncoder)
         self.assertEqual(valid_memo, range_factory.parseMemo(json_data))
         self.assertEqual(0, len(self.error_messages))
 
@@ -392,7 +392,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset = self.makeStormResultSet()
         resultset.order_by(Desc(Person.id))
         range_factory = StormRangeFactory(resultset, self.logError)
-        self.assertEqual([1], range_factory.parseMemo(simplejson.dumps([1])))
+        self.assertEqual([1], range_factory.parseMemo(json.dumps([1])))
 
     def test_reverseSortOrder(self):
         # reverseSortOrder() wraps a plain PropertyColumn instance into
@@ -584,7 +584,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset = self.makeStormResultSet()
         resultset.order_by(Person.name, Person.id)
         all_results = list(resultset)
-        memo = simplejson.dumps([all_results[0].name, all_results[0].id])
+        memo = json.dumps([all_results[0].name, all_results[0].id])
         range_factory = StormRangeFactory(resultset)
         sliced_result = range_factory.getSlice(3, memo)
         self.assertEqual(all_results[1:4], list(sliced_result))
@@ -605,7 +605,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         all_results = list(resultset)
         expected = all_results[1:4]
         expected.reverse()
-        memo = simplejson.dumps([all_results[4].name, all_results[4].id])
+        memo = json.dumps([all_results[4].name, all_results[4].id])
         range_factory = StormRangeFactory(resultset)
         sliced_result = range_factory.getSlice(3, memo, forwards=False)
         self.assertEqual(expected, list(sliced_result))
@@ -646,9 +646,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         # for a forward batch, the slice of this btach starts with
         # the fourth row of the entire result set.
         memo_lfa = all_results[2].libraryfile
-        memo = simplejson.dumps(
-            [memo_lfa.mimetype, memo_lfa.filename, memo_lfa.id]
-        )
+        memo = json.dumps([memo_lfa.mimetype, memo_lfa.filename, memo_lfa.id])
         range_factory = StormRangeFactory(resultset)
         sliced_result = range_factory.getSlice(3, memo)
         self.assertEqual(all_results[3:6], list(sliced_result))
@@ -658,7 +656,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset.order_by(LibraryFileAlias.id)
         all_results = list(resultset)
         plain_results = list(resultset.get_plain_result_set())
-        memo = simplejson.dumps([resultset.get_plain_result_set()[0][1].id])
+        memo = json.dumps([resultset.get_plain_result_set()[0][1].id])
         range_factory = StormRangeFactory(resultset)
         sliced_result = range_factory.getSlice(3, memo)
         self.assertEqual(all_results[1:4], list(sliced_result))
@@ -677,7 +675,7 @@ class TestStormRangeFactory(TestCaseWithFactory):
         resultset = self.makeStormResultSet()
         resultset.order_by(Person.id)
         all_results = list(resultset)
-        memo = simplejson.dumps([all_results[2].id])
+        memo = json.dumps([all_results[2].id])
         range_factory = StormRangeFactory(resultset)
         backward_slice = range_factory.getSlice(
             size=2, endpoint_memo=memo, forwards=False
diff --git a/lib/lp/services/webapp/tests/test_view_model.py b/lib/lp/services/webapp/tests/test_view_model.py
index 15ce566..f7cc65a 100644
--- a/lib/lp/services/webapp/tests/test_view_model.py
+++ b/lib/lp/services/webapp/tests/test_view_model.py
@@ -3,9 +3,10 @@
 
 """Tests for the user requested oops using ++oops++ traversal."""
 
+import json
+
 from lazr.restful.interfaces import IJSONRequestCache
 from lazr.restful.utils import get_current_browser_request
-from simplejson import loads
 from testtools.matchers import KeysEqual
 from zope.configuration import xmlconfig
 
@@ -123,7 +124,7 @@ class TestJsonModelView(BrowserTestCase):
         lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
         self.configZCML()
         browser = self.getUserBrowser(self.url)
-        cache = loads(browser.contents)
+        cache = json.loads(browser.contents.decode())
         self.assertThat(cache, KeysEqual("related_features", "context"))
 
     def test_JsonModel_custom_cache(self):
@@ -140,7 +141,7 @@ class TestJsonModelView(BrowserTestCase):
         lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
         self.configZCML()
         browser = self.getUserBrowser(self.url)
-        cache = loads(browser.contents)
+        cache = json.loads(browser.contents.decode())
         self.assertThat(
             cache, KeysEqual("related_features", "context", "target_info")
         )
@@ -165,7 +166,7 @@ class TestJsonModelView(BrowserTestCase):
         lp.services.webapp.tests.ProductModelTestView = ProductModelTestView
         self.configZCML()
         browser = self.getUserBrowser(self.url)
-        cache = loads(browser.contents)
+        cache = json.loads(browser.contents.decode())
         self.assertThat(
             cache, KeysEqual("related_features", "context", "target_info")
         )
diff --git a/lib/lp/services/webservice/stories/conditional-write.rst b/lib/lp/services/webservice/stories/conditional-write.rst
index 2ee3197..6e25c81 100644
--- a/lib/lp/services/webservice/stories/conditional-write.rst
+++ b/lib/lp/services/webservice/stories/conditional-write.rst
@@ -70,8 +70,8 @@ modify directly.
 So long as the second part of the submitted ETag matches, a
 conditional write will succeed.
 
-    >>> import simplejson
-    >>> data = simplejson.dumps({"title": "New title"})
+    >>> import json
+    >>> data = json.dumps({"title": "New title"})
     >>> headers = {"If-Match": old_etag}
     >>> print(
     ...     webservice.patch(url, "application/json", data, headers=headers)
diff --git a/lib/lp/services/webservice/stories/xx-service.rst b/lib/lp/services/webservice/stories/xx-service.rst
index 11eef09..8e655d5 100644
--- a/lib/lp/services/webservice/stories/xx-service.rst
+++ b/lib/lp/services/webservice/stories/xx-service.rst
@@ -86,8 +86,8 @@ Anonymous requests can't access certain data.
 
 Anonymous requests can't change the dataset.
 
-    >>> import simplejson
-    >>> data = simplejson.dumps({"display_name": "This won't work"})
+    >>> import json
+    >>> data = json.dumps({"display_name": "This won't work"})
     >>> response = anon_webservice.patch(
     ...     root + "/~salgado", "application/json", data
     ... )
diff --git a/lib/lp/soyuz/model/packagediffjob.py b/lib/lp/soyuz/model/packagediffjob.py
index b673c35..f4c20ac 100644
--- a/lib/lp/soyuz/model/packagediffjob.py
+++ b/lib/lp/soyuz/model/packagediffjob.py
@@ -5,7 +5,8 @@ __all__ = [
     "PackageDiffJob",
 ]
 
-import simplejson
+import json
+
 from lazr.delegates import delegate_to
 from zope.component import getUtility
 from zope.interface import implementer, provider
@@ -38,7 +39,7 @@ class PackageDiffJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
         job = Job(
             base_job_type=JobType.GENERATE_PACKAGE_DIFF,
             requester=packagediff.requester,
-            base_json_data=simplejson.dumps({"packagediff": packagediff.id}),
+            base_json_data=json.dumps({"packagediff": packagediff.id}),
         )
         derived = cls(job)
         derived.celeryRunOnCommit()
@@ -75,7 +76,7 @@ class PackageDiffJob(PackageDiffJobDerived):
 
     @property
     def packagediff_id(self):
-        return simplejson.loads(self.base_json_data)["packagediff"]
+        return json.loads(self.base_json_data)["packagediff"]
 
     @property
     def packagediff(self):
diff --git a/lib/lp/soyuz/stories/webservice/xx-archive.rst b/lib/lp/soyuz/stories/webservice/xx-archive.rst
index 0510710..9ea0d65 100644
--- a/lib/lp/soyuz/stories/webservice/xx-archive.rst
+++ b/lib/lp/soyuz/stories/webservice/xx-archive.rst
@@ -1391,14 +1391,11 @@ Non-virtualized archives
 Modifying the require_virtualized flag through the API is not allowed except
 for admins, commercial admins, and PPA admins.
 
-    >>> import simplejson
+    >>> import json
     >>> def modify_archive(service, archive):
     ...     headers = {"Content-type": "application/json"}
     ...     return service(
-    ...         archive["self_link"],
-    ...         "PUT",
-    ...         simplejson.dumps(archive),
-    ...         headers,
+    ...         archive["self_link"], "PUT", json.dumps(archive), headers
     ...     )
     ...
 
diff --git a/lib/lp/soyuz/stories/webservice/xx-archivedependency.rst b/lib/lp/soyuz/stories/webservice/xx-archivedependency.rst
index defcf2f..46b5c5e 100644
--- a/lib/lp/soyuz/stories/webservice/xx-archivedependency.rst
+++ b/lib/lp/soyuz/stories/webservice/xx-archivedependency.rst
@@ -12,7 +12,7 @@ We'll use Celso's PPA, and give it a custom dependency on the primary
 archive, and then create a private PPA for Celso with a similar custom
 dependency.
 
-    >>> import simplejson
+    >>> import json
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -95,7 +95,7 @@ But even he can't write to a dependency.
     ...     cprov_webservice.patch(
     ...         "/~cprov/+archive/ubuntu/ppa/+dependency/1",
     ...         "application/json",
-    ...         simplejson.dumps({"archive_link": mark_ppa["self_link"]}),
+    ...         json.dumps({"archive_link": mark_ppa["self_link"]}),
     ...     )
     ... )
     HTTP/1.1 400 Bad Request
@@ -107,7 +107,7 @@ But even he can't write to a dependency.
     ...     cprov_webservice.patch(
     ...         "/~cprov/+archive/ubuntu/ppa/+dependency/1",
     ...         "application/json",
-    ...         simplejson.dumps({"dependency_link": mark_ppa["self_link"]}),
+    ...         json.dumps({"dependency_link": mark_ppa["self_link"]}),
     ...     )
     ... )
     HTTP/1.1 400 Bad Request
@@ -119,7 +119,7 @@ But even he can't write to a dependency.
     ...     cprov_webservice.patch(
     ...         "/~cprov/+archive/ubuntu/ppa/+dependency/1",
     ...         "application/json",
-    ...         simplejson.dumps({"pocket": "Security"}),
+    ...         json.dumps({"pocket": "Security"}),
     ...     )
     ... )
     HTTP/1.1 400 Bad Request
diff --git a/lib/lp/soyuz/stories/webservice/xx-packageset.rst b/lib/lp/soyuz/stories/webservice/xx-packageset.rst
index 4ea90ed..b9bb81a 100644
--- a/lib/lp/soyuz/stories/webservice/xx-packageset.rst
+++ b/lib/lp/soyuz/stories/webservice/xx-packageset.rst
@@ -113,7 +113,7 @@ Let's create another set.
 
 We can modify it, and even give it away.
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> name16 = webservice.get("/~name16").jsonBody()
     >>> patch = {
     ...     "name": "renamed",
@@ -123,7 +123,7 @@ We can modify it, and even give it away.
     >>> response = webservice.patch(
     ...     "/package-sets/ubuntu/hoary/shortlived",
     ...     "application/json",
-    ...     dumps(patch),
+    ...     json.dumps(patch),
     ... )
     >>> print(response)
     HTTP/1.1 301 Moved Permanently
diff --git a/lib/lp/soyuz/tests/test_binarypackagebuild.py b/lib/lp/soyuz/tests/test_binarypackagebuild.py
index 41791bd..ef513d3 100644
--- a/lib/lp/soyuz/tests/test_binarypackagebuild.py
+++ b/lib/lp/soyuz/tests/test_binarypackagebuild.py
@@ -3,11 +3,11 @@
 
 """Test Build features."""
 
+import json
 from datetime import datetime, timedelta
 
 import pytz
 from pymacaroons import Macaroon
-from simplejson import dumps
 from testtools.matchers import Equals, MatchesListwise, MatchesStructure
 from zope.component import getUtility
 from zope.publisher.xmlrpc import TestRequest
@@ -647,7 +647,7 @@ class TestBinaryPackageBuildWebservice(TestCaseWithFactory):
         response = webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps({"external_dependencies": "random"}),
+            json.dumps({"external_dependencies": "random"}),
         )
         self.assertEqual(401, response.status)
 
@@ -660,7 +660,7 @@ class TestBinaryPackageBuildWebservice(TestCaseWithFactory):
         response = self.webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps({"external_dependencies": "random"}),
+            json.dumps({"external_dependencies": "random"}),
         )
         self.assertEqual(401, response.status)
 
@@ -678,7 +678,7 @@ class TestBinaryPackageBuildWebservice(TestCaseWithFactory):
         response = webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps({"external_dependencies": "random"}),
+            json.dumps({"external_dependencies": "random"}),
         )
         self.assertEqual(400, response.status)
         self.assertIn(b"Invalid external dependencies", response.body)
@@ -698,7 +698,7 @@ class TestBinaryPackageBuildWebservice(TestCaseWithFactory):
         response = webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps({"external_dependencies": dependencies}),
+            json.dumps({"external_dependencies": dependencies}),
         )
         self.assertEqual(209, response.status)
         self.assertEqual(
@@ -958,7 +958,7 @@ class TestCalculateScore(TestCaseWithFactory):
         response = webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps(dict(relative_build_score=100)),
+            json.dumps(dict(relative_build_score=100)),
         )
         self.assertEqual(401, response.status)
         new_entry = webservice.get(obj_url, api_version="devel").jsonBody()
@@ -976,7 +976,7 @@ class TestCalculateScore(TestCaseWithFactory):
         response = webservice.patch(
             entry["self_link"],
             "application/json",
-            dumps(dict(relative_build_score=100)),
+            json.dumps(dict(relative_build_score=100)),
         )
         self.assertEqual(209, response.status)
         self.assertEqual(100, response.jsonBody()["relative_build_score"])
diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
index 4133a2a..4b78f9a 100644
--- a/lib/lp/testing/__init__.py
+++ b/lib/lp/testing/__init__.py
@@ -52,6 +52,7 @@ __all__ = [
 ]
 
 import io
+import json
 import logging
 import os
 import re
@@ -72,7 +73,6 @@ import fixtures
 import lp_sitecustomize
 import oops_datedir_repo.serializer_rfc822
 import pytz
-import simplejson
 import six
 import subunit
 import testtools
@@ -1544,7 +1544,7 @@ def extract_lp_cache(text):
     match = re.search(r"<script[^>]*>LP.cache = (\{.*\});</script>", text)
     if match is None:
         raise ValueError("No JSON cache found.")
-    return simplejson.loads(match.group(1))
+    return json.loads(match.group(1))
 
 
 def nonblocking_readline(instream, timeout):
diff --git a/lib/lp/testing/tests/test_yuixhr.py b/lib/lp/testing/tests/test_yuixhr.py
index eea4635..3e93964 100644
--- a/lib/lp/testing/tests/test_yuixhr.py
+++ b/lib/lp/testing/tests/test_yuixhr.py
@@ -3,6 +3,7 @@
 
 """Tests for the lp.testing.yuixhr."""
 
+import json
 import os
 import re
 import sys
@@ -10,7 +11,6 @@ import tempfile
 import types
 from shutil import rmtree
 
-import simplejson
 import transaction
 from storm.exceptions import DisconnectionError
 from testtools.testcase import ExpectedException
@@ -207,7 +207,7 @@ class TestYUITestFixtureController(TestCase):
             method="POST",
         )
         content = view()
-        self.assertEqual({"hello": "world"}, simplejson.loads(content))
+        self.assertEqual({"hello": "world"}, json.loads(content))
         self.assertEqual(
             "application/json", view.request.response.getHeader("Content-Type")
         )
@@ -218,7 +218,7 @@ class TestYUITestFixtureController(TestCase):
             form={"action": "setup", "fixtures": "make_product"},
             method="POST",
         )
-        data = simplejson.loads(view())
+        data = json.loads(view())
         # The licenses is just an example.
         self.assertEqual(["GNU GPL v2"], data["product"]["licenses"])
 
@@ -228,7 +228,7 @@ class TestYUITestFixtureController(TestCase):
             form={"action": "setup", "fixtures": "make_product"},
             method="POST",
         )
-        data = simplejson.loads(view())
+        data = json.loads(view())
         self.assertEqual(
             "tag:launchpad.net:2008:redacted",
             data["product"]["project_reviewed"],
@@ -240,7 +240,7 @@ class TestYUITestFixtureController(TestCase):
             form={"action": "setup", "fixtures": "naughty_make_product"},
             method="POST",
         )
-        data = simplejson.loads(view())
+        data = json.loads(view())
         self.assertEqual(
             "tag:launchpad.net:2008:redacted",
             data["product"]["project_reviewed"],
@@ -252,7 +252,7 @@ class TestYUITestFixtureController(TestCase):
             form={"action": "setup", "fixtures": "make_product_loggedin"},
             method="POST",
         )
-        data = simplejson.loads(view())
+        data = json.loads(view())
         self.assertEqual(False, data["product"]["project_reviewed"])
 
     def test_add_cleanup_decorator(self):
@@ -288,7 +288,7 @@ class TestYUITestFixtureController(TestCase):
             form={
                 "action": "teardown",
                 "fixtures": "baseline",
-                "data": simplejson.dumps({"bonjour": "monde"}),
+                "data": json.dumps({"bonjour": "monde"}),
             },
             method="POST",
         )
@@ -317,7 +317,7 @@ class TestYUITestFixtureController(TestCase):
         # Committing the transaction makes sure that we are not just seeing
         # the effect of an abort, below.
         transaction.commit()
-        name = simplejson.loads(data)["product"]["name"]
+        name = json.loads(data)["product"]["name"]
         products = getUtility(IProductSet)
         # The new product exists after the setup.
         self.assertFalse(products.getByName(name) is None)
@@ -350,7 +350,7 @@ class TestYUITestFixtureController(TestCase):
             form={
                 "action": "teardown",
                 "fixtures": "baseline,second",
-                "data": simplejson.dumps({"bonjour": "monde"}),
+                "data": json.dumps({"bonjour": "monde"}),
             },
             method="POST",
         )
diff --git a/lib/lp/translations/browser/hastranslationimports.py b/lib/lp/translations/browser/hastranslationimports.py
index c86d1c3..c1f7623 100644
--- a/lib/lp/translations/browser/hastranslationimports.py
+++ b/lib/lp/translations/browser/hastranslationimports.py
@@ -8,9 +8,9 @@ __all__ = [
 ]
 
 import datetime
+import json
 
 import pytz
-import simplejson
 from zope.browserpage import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.formlib import form
@@ -379,7 +379,7 @@ class HasTranslationImportsView(LaunchpadFormView):
         for entry in self.batchnav.batch:
             if check_permission("launchpad.Edit", entry):
                 confs.append(self.generateChoiceConfForEntry(entry))
-        return "var choice_confs = %s;" % simplejson.dumps(confs)
+        return "var choice_confs = %s;" % json.dumps(confs)
 
     def generateChoiceConfForEntry(self, entry):
         disabled_items = [
diff --git a/lib/lp/translations/stories/webservice/xx-translationfocus.rst b/lib/lp/translations/stories/webservice/xx-translationfocus.rst
index fcd393a..110f6a1 100644
--- a/lib/lp/translations/stories/webservice/xx-translationfocus.rst
+++ b/lib/lp/translations/stories/webservice/xx-translationfocus.rst
@@ -15,12 +15,12 @@ outside the scope of this test.
 It's possible to set the translation focus through the API
 if you're an admin. The translation focus should be a project series.
 
-    >>> from simplejson import dumps
+    >>> import json
     >>> print(
     ...     webservice.patch(
     ...         evolution["self_link"],
     ...         "application/json",
-    ...         dumps(
+    ...         json.dumps(
     ...             {
     ...                 "translation_focus_link": evolution[
     ...                     "development_focus_link"
@@ -43,7 +43,7 @@ Unprivileged users cannot set the translation focus.
     ...     user_webservice.patch(
     ...         evolution["self_link"],
     ...         "application/json",
-    ...         dumps({"translation_focus_link": None}),
+    ...         json.dumps({"translation_focus_link": None}),
     ...     )
     ... )
     HTTP... 401 Unauthorized
diff --git a/lib/lp/translations/stories/webservice/xx-translationimportqueue.rst b/lib/lp/translations/stories/webservice/xx-translationimportqueue.rst
index 63339d0..a17875a 100644
--- a/lib/lp/translations/stories/webservice/xx-translationimportqueue.rst
+++ b/lib/lp/translations/stories/webservice/xx-translationimportqueue.rst
@@ -85,7 +85,7 @@ Entry fields
 Most of the fields in a translation import queue entry are immutable
 from the web service's point of view.
 
-    >>> from simplejson import dumps
+    >>> import json
 
 
 Path
@@ -96,7 +96,9 @@ An entry's file path can be changed by the entry's owner or an admin.
     >>> first_entry = queue["entries"][0]["self_link"]
     >>> print(
     ...     webservice.patch(
-    ...         first_entry, "application/json", dumps({"path": "foo.pot"})
+    ...         first_entry,
+    ...         "application/json",
+    ...         json.dumps({"path": "foo.pot"}),
     ...     )
     ... )
     HTTP/1.1 209 Content Returned
@@ -110,7 +112,9 @@ A regular user is not allowed to make this change.
     >>> first_entry = queue["entries"][0]["self_link"]
     >>> print(
     ...     user_webservice.patch(
-    ...         first_entry, "application/json", dumps({"path": "bar.pot"})
+    ...         first_entry,
+    ...         "application/json",
+    ...         json.dumps({"path": "bar.pot"}),
     ...     )
     ... )
     HTTP... Unauthorized
@@ -125,7 +129,9 @@ For now, it is not possible to set an entry's status through the API.
     >>> first_entry = queue["entries"][0]["self_link"]
     >>> print(
     ...     webservice.patch(
-    ...         first_entry, "application/json", dumps({"status": "Approved"})
+    ...         first_entry,
+    ...         "application/json",
+    ...         json.dumps({"status": "Approved"}),
     ...     )
     ... )
     HTTP... Bad Request
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index f1efa07..30b4cfe 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -156,7 +156,6 @@ service-identity==18.1.0
 setproctitle==1.1.7
 setuptools-git==1.2
 setuptools-scm==3.4.3
-simplejson==3.8.2
 soupmatchers==0.4
 soupsieve==1.9
 statsd==3.3.0
diff --git a/requirements/types.txt b/requirements/types.txt
index 79b6d5e..752d982 100644
--- a/requirements/types.txt
+++ b/requirements/types.txt
@@ -4,5 +4,4 @@ types-beautifulsoup4==4.9.0
 types-bleach==3.3.1
 types-pytz==0.1.0
 types-requests==0.1.13
-types-simplejson==0.1.0
 types-six==0.1.9
diff --git a/setup.cfg b/setup.cfg
index e69cbc5..a13cb50 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -100,7 +100,6 @@ install_requires =
     selenium
     setproctitle
     setuptools
-    simplejson
     six
     soupmatchers
     Sphinx