diff --git a/src/_zkapauthorizer/_json.py b/src/_zkapauthorizer/_json.py
index ff8c0c2070e275b3761038c868540552b8c11146..898f6b5601e0647e62a262e55272946d514228c6 100644
--- a/src/_zkapauthorizer/_json.py
+++ b/src/_zkapauthorizer/_json.py
@@ -1,34 +1,22 @@
-from __future__ import absolute_import, division, print_function, unicode_literals
+# Copyright 2022 PrivateStorage.io, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
-from future.utils import PY2
-
-if PY2:
-    from future.builtins import (  # noqa: F401
-        filter,
-        map,
-        zip,
-        ascii,
-        chr,
-        hex,
-        input,
-        next,
-        oct,
-        open,
-        pow,
-        round,
-        super,
-        bytes,
-        dict,
-        list,
-        object,
-        range,
-        str,
-        max,
-        min,
-    )
-
-from six import ensure_binary
 from json import dumps as _dumps
+from typing import Any
 
-def dumps(o):
-    return ensure_binary(_dumps(o))
+def dumps_utf8(o: Any) -> bytes:
+    """
+    Serialize an object to a UTF-8-encoded JSON byte string.
+    """
+    return _dumps(o).encode("utf-8")
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index e11fd5f8b6429b01bedff57ad140175e4717ab04..4be61aa6a6d9058cb3a4cbf77705f485e32fb979 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -50,7 +50,7 @@ from base64 import b64decode, b64encode
 from datetime import timedelta
 from functools import partial
 from hashlib import sha256
-from json import dumps, loads
+from json import loads
 from operator import delitem, setitem
 from sys import exc_info
 
@@ -67,6 +67,7 @@ from twisted.python.url import URL
 from twisted.web.client import Agent
 from zope.interface import Interface, implementer
 
+from ._json import dumps_utf8
 from ._base64 import urlsafe_b64decode
 from ._stack import less_limited_stack
 from .model import Error as model_Error
@@ -530,7 +531,7 @@ class RistrettoRedeemer(object):
         blinded_tokens = list(token.blind() for token in random_tokens)
         response = yield self._treq.post(
             self._api_root.child("v1", "redeem").to_text(),
-            dumps(
+            dumps_utf8(
                 {
                     "redeemVoucher": voucher.number.decode("ascii"),
                     "redeemCounter": counter,
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index fba2112dcb9146082f723930a7188b27ccc068ee..0aec9d3885a790c847642efc141d692fd0c81f0e 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -60,7 +60,7 @@ from twisted.python.filepath import FilePath
 from zope.interface import Interface, implementer
 from six import ensure_text
 
-from ._json import dumps
+from ._json import dumps_utf8
 from ._base64 import urlsafe_b64decode
 from .schema import get_schema_upgrades, get_schema_version, run_schema_upgrades
 from .storage_common import (
@@ -1199,7 +1199,7 @@ class Voucher(object):
         )
 
     def to_json(self):
-        return dumps(self.marshal())
+        return dumps_utf8(self.marshal())
 
     def marshal(self):
         return self.to_json_v1()
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 82d787e545129aff07609e02894a72d1022df621..b2b97520feec4b99da866f4b947d500b704e6c01 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -62,7 +62,7 @@ from twisted.web.resource import ErrorPage, IResource, NoResource, Resource
 from twisted.web.server import NOT_DONE_YET
 from zope.interface import Attribute
 
-from ._json import dumps
+from ._json import dumps_utf8
 from . import __version__ as _zkapauthorizer_version
 from ._base64 import urlsafe_b64decode
 from .config import get_configured_lease_duration
@@ -260,7 +260,7 @@ class _CalculatePrice(Resource):
             body_object = loads(payload)
         except ValueError:
             request.setResponseCode(BAD_REQUEST)
-            return dumps(
+            return dumps_utf8(
                 {
                     "error": "could not parse request body",
                 }
@@ -271,7 +271,7 @@ class _CalculatePrice(Resource):
             sizes = body_object["sizes"]
         except (TypeError, KeyError):
             request.setResponseCode(BAD_REQUEST)
-            return dumps(
+            return dumps_utf8(
                 {
                     "error": "could not read `version` and `sizes` properties",
                 }
@@ -279,7 +279,7 @@ class _CalculatePrice(Resource):
 
         if version != 1:
             request.setResponseCode(BAD_REQUEST)
-            return dumps(
+            return dumps_utf8(
                 {
                     "error": "did not find required version number 1 in request",
                 }
@@ -289,7 +289,7 @@ class _CalculatePrice(Resource):
             isinstance(size, (int, long)) and size >= 0 for size in sizes
         ):
             request.setResponseCode(BAD_REQUEST)
-            return dumps(
+            return dumps_utf8(
                 {
                     "error": "did not find required positive integer sizes list in request",
                 }
@@ -298,7 +298,7 @@ class _CalculatePrice(Resource):
         application_json(request)
 
         price = self._price_calculator.calculate(sizes)
-        return dumps(
+        return dumps_utf8(
             {
                 "price": price,
                 "period": self._lease_period,
@@ -345,7 +345,7 @@ class _ProjectVersion(Resource):
 
     def render_GET(self, request):
         application_json(request)
-        return dumps(
+        return dumps_utf8(
             {
                 "version": _zkapauthorizer_version,
             }
@@ -379,7 +379,7 @@ class _UnblindedTokenCollection(Resource):
 
         position = request.args.get(b"position", [b""])[0].decode("utf-8")
 
-        return dumps(
+        return dumps_utf8(
             {
                 "total": len(unblinded_tokens),
                 "spendable": self._store.count_unblinded_tokens(),
@@ -399,7 +399,7 @@ class _UnblindedTokenCollection(Resource):
         application_json(request)
         unblinded_tokens = load(request.content)["unblinded-tokens"]
         self._store.insert_unblinded_tokens(unblinded_tokens, group_id=0)
-        return dumps({})
+        return dumps_utf8({})
 
     def _lease_maintenance_activity(self):
         activity = self._store.get_latest_lease_maintenance_activity()
@@ -452,7 +452,7 @@ class _VoucherCollection(Resource):
 
     def render_GET(self, request):
         application_json(request)
-        return dumps(
+        return dumps_utf8(
             {
                 "vouchers": list(
                     self._controller.incorporate_transient_state(voucher).marshal()
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 471a41a7d5f28feed3a5238bb5c4990fbe43e386..63acf4c4bc59e486ff23f2a7fb4b302d90599e29 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -98,7 +98,7 @@ from twisted.web.http import BAD_REQUEST, NOT_FOUND, NOT_IMPLEMENTED, OK, UNAUTH
 from twisted.web.http_headers import Headers
 from twisted.web.resource import IResource, getChildForRequest
 
-from .. _json import dumps
+from .. _json import dumps_utf8
 from .. import __version__ as zkapauthorizer_version
 from .._base64 import urlsafe_b64decode
 from ..configutil import config_string_from_sections
@@ -204,7 +204,7 @@ def invalid_bodies():
             {
                 "some-key": vouchers().map(ensure_text),
             }
-        ).map(dumps),
+        ).map(dumps_utf8),
         # The right key but the wrong kind of value.
         fixed_dictionaries(
             {
@@ -213,7 +213,7 @@ def invalid_bodies():
                     not_vouchers().map(ensure_text),
                 ),
             }
-        ).map(dumps),
+        ).map(dumps_utf8),
         # Not even JSON
         binary().filter(is_not_json),
     )
@@ -518,7 +518,7 @@ class UnblindedTokenTests(TestCase):
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
         data = BytesIO(
-            dumps(
+            dumps_utf8(
                 {
                     "unblinded-tokens": list(
                         token.unblinded_token.decode("ascii")
@@ -970,7 +970,7 @@ class VoucherTests(TestCase):
         )
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
-        data = BytesIO(dumps({"voucher": voucher.decode("ascii")}))
+        data = BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")}))
         requesting = authorized_request(
             api_auth_token,
             agent,
@@ -1263,7 +1263,7 @@ class VoucherTests(TestCase):
             agent,
             b"PUT",
             b"http://127.0.0.1/voucher",
-            data=BytesIO(dumps({"voucher": voucher.decode("ascii")})),
+            data=BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")})),
         )
         self.assertThat(
             putting,
@@ -1390,7 +1390,7 @@ class VoucherTests(TestCase):
         note("{} vouchers".format(len(vouchers)))
 
         for voucher in vouchers:
-            data = BytesIO(dumps({"voucher": voucher.decode("ascii")}))
+            data = BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")}))
             putting = authorized_request(
                 api_auth_token,
                 agent,
@@ -1511,26 +1511,26 @@ def bad_calculate_price_requests():
             "version": good_version,
             "sizes": good_sizes,
         }
-    ).map(dumps)
+    ).map(dumps_utf8)
 
     bad_data_version = fixed_dictionaries(
         {
             "version": bad_version,
             "sizes": good_sizes,
         }
-    ).map(dumps)
+    ).map(dumps_utf8)
 
     bad_data_sizes = fixed_dictionaries(
         {
             "version": good_version,
             "sizes": bad_sizes,
         }
-    ).map(dumps)
+    ).map(dumps_utf8)
 
     bad_data_other = dictionaries(
         text(),
         integers(),
-    ).map(dumps)
+    ).map(dumps_utf8)
 
     bad_data_junk = binary()
 
@@ -1667,7 +1667,7 @@ class CalculatePriceTests(TestCase):
                 b"POST",
                 self.url,
                 headers={b"content-type": [b"application/json"]},
-                data=BytesIO(dumps({"version": 1, "sizes": sizes})),
+                data=BytesIO(dumps_utf8({"version": 1, "sizes": sizes})),
             ),
             succeeded(
                 matches_response(
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 4b4f399b1ff7965a43b700d066bce888a719cd0d..1ffac55a631a43011875c820d2b140bf2b5d4b33 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -59,7 +59,7 @@ from twisted.web.iweb import IAgent
 from twisted.web.resource import ErrorPage, Resource
 from zope.interface import implementer
 
-from .._json import dumps
+from .._json import dumps_utf8
 from ..controller import (
     AlreadySpent,
     DoubleSpendRedeemer,
@@ -1155,7 +1155,7 @@ class RistrettoRedemption(Resource):
         finally:
             servers_proof.destroy()
 
-        return dumps(
+        return dumps_utf8(
             {
                 u"success": True,
                 u"public-key": ensure_text(self.public_key.encode_base64()),
@@ -1233,7 +1233,7 @@ class CheckRedemptionRequestTests(TestCase):
         treq = treq_for_loopback_ristretto(issuer)
         d = treq.post(
             NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
-            dumps(dict.fromkeys(properties)),
+            dumps_utf8(dict.fromkeys(properties)),
             headers=Headers({u"content-type": [u"application/json"]}),
         )
         self.assertThat(
@@ -1283,7 +1283,7 @@ def check_redemption_request(request):
 def bad_request(request, body_object):
     request.setResponseCode(BAD_REQUEST)
     request.setHeader(b"content-type", b"application/json")
-    request.write(dumps(body_object))
+    request.write(dumps_utf8(body_object))
     return b""