diff --git a/ip.cabal b/ip.cabal
--- a/ip.cabal
+++ b/ip.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: ip
-version: 1.7.3
+version: 1.7.4
 synopsis: Library for IP and MAC addresses
 homepage: https://github.com/andrewthad/haskell-ip#readme
 license: BSD-3-Clause
@@ -46,7 +46,7 @@
     Data.Text.Builder.Variable
     Data.Word.Synthetic.Word12
   build-depends:
-    , aeson >= 1.0 && < 1.6
+    , aeson >= 1.0 && < 2.1
     , attoparsec >= 0.13 && < 0.14
     , base >= 4.9 && < 5
     , byteslice >= 0.1.2 && < 0.3
diff --git a/src/Data/Word/Synthetic/Word12.hs b/src/Data/Word/Synthetic/Word12.hs
--- a/src/Data/Word/Synthetic/Word12.hs
+++ b/src/Data/Word/Synthetic/Word12.hs
@@ -33,7 +33,12 @@
 import           GHC.Arr
 import           GHC.Base
 import           GHC.Enum
+#if MIN_VERSION_base(4,15,0)
+import           GHC.Integer (integerToWord, smallInteger)
+import           GHC.Num hiding (integerToWord)
+#else
 import           GHC.Num
+#endif
 import           GHC.Read
 import           GHC.Real
 import           GHC.Show
diff --git a/src/Net/IP.hs b/src/Net/IP.hs
--- a/src/Net/IP.hs
+++ b/src/Net/IP.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE DeriveDataTypeable #-}
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveGeneric #-}
@@ -46,6 +47,7 @@
   , decodeShort
   , boundedBuilderUtf8
     -- ** Bytes
+  , decodeUtf8Bytes
   , parserUtf8Bytes
     -- ** Printing
   , print
@@ -72,6 +74,7 @@
 
 import qualified Arithmetic.Lte as Lte
 import qualified Data.Aeson as Aeson
+import qualified Data.Bytes as Bytes
 import qualified Data.Bytes.Builder.Bounded as BB
 import qualified Data.Text.IO as TIO
 import qualified Data.Bytes.Parser as Parser
@@ -178,6 +181,14 @@
 decodeShort t
   | Just x <- IPv4.decodeShort t = Just (fromIPv4 x)
   | otherwise = coerce (IPv6.decodeShort t)
+
+-- | Decode UTF-8-encoded 'Bytes' into an 'IP' address.
+decodeUtf8Bytes :: Bytes.Bytes -> Maybe IP
+decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of
+  Parser.Success (Parser.Slice _ len addr) -> case len of
+    0 -> Just addr
+    _ -> Nothing
+  Parser.Failure _ -> Nothing
 
 -- | Parse UTF-8-encoded 'Bytes' as an 'IP' address.
 parserUtf8Bytes :: e -> Parser.Parser e s IP
diff --git a/src/Net/IPv4.hs b/src/Net/IPv4.hs
--- a/src/Net/IPv4.hs
+++ b/src/Net/IPv4.hs
@@ -137,6 +137,7 @@
 import qualified Data.Bytes as Bytes
 import qualified Data.Bytes.Parser as Parser
 import qualified Data.Bytes.Parser.Latin as Latin
+import qualified Data.Char as Char
 import qualified Data.Primitive as PM
 import qualified Data.Text as Text
 import qualified Data.Text.Array as TArray
@@ -153,6 +154,10 @@
 import qualified Data.Vector.Unboxed as UVector
 import qualified Data.Vector.Unboxed.Mutable as MUVector
 
+#if MIN_VERSION_aeson(2,0,0)
+import qualified Data.Aeson.Key as AesonKey
+#endif
+
 -- $setup
 --
 -- These are here to get doctest's property checking to work
@@ -697,8 +702,14 @@
 
 instance ToJSONKey IPv4 where
   toJSONKey = ToJSONKeyText
-    encode
+    (keyFromText . encode)
     (\addr -> Aeson.unsafeToEncoding $ Builder.char7 '"' <> builderUtf8 addr <> Builder.char7 '"')
+    where
+#if MIN_VERSION_aeson(2,0,0)
+      keyFromText = AesonKey.fromText
+#else
+      keyFromText = id
+#endif
 
 instance FromJSONKey IPv4 where
   fromJSONKey = FromJSONKeyTextParser aesonParser
@@ -721,16 +732,47 @@
 
 decodeIPv4TextReader :: TextRead.Reader IPv4
 decodeIPv4TextReader t1' = do
-  (a,t2) <- TextRead.decimal t1'
+  (a,t2) <- readOctet t1'
   t2' <- stripDecimal t2
-  (b,t3) <- TextRead.decimal t2'
+  (b,t3) <- readOctet t2'
   t3' <- stripDecimal t3
-  (c,t4) <- TextRead.decimal t3'
+  (c,t4) <- readOctet t3'
   t4' <- stripDecimal t4
-  (d,t5) <- TextRead.decimal t4'
-  if a > 255 || b > 255 || c > 255 || d > 255
-    then Left ipOctetSizeErrorMsg
-    else Right (fromOctets' a b c d,t5)
+  (d,t5) <- readOctet t4'
+  Right (fromOctets' a b c d,t5)
+
+-- | Read an IPv4 octet (@0 <= n <= 255@)
+--
+-- The input must begin with at least one decimal digit.  Input is consumed
+-- until a non-digit is reached, the end of the input is reached, or the
+-- accumulated value exceeds the maximum bound (255).  As with
+-- 'TextRead.decimal', any number of leading zeros are permitted.
+--
+-- Optimizations:
+--
+-- * The 'Char.isDigit' and 'Char.digitToInt' functions are avoided in order
+--   to avoiding checking the range more than once.  This implementation calls
+--   'Char.ord' (once) and uses the result for both the range check and the
+--   calculation.
+-- * The type of the accumulated value is 'Int', allowing for a single
+--   'fromIntegral' call instead of one for each digit.  This is possible
+--   because the maximum bound (255) is sufficiently less than the maximum
+--   bound of 'Int'.  Specifically: @255 * 10 + Char.ord '9' <= maxBound@
+-- * This implementation does not make use of @UnboxedTuples@ because the
+--   @span_@ function is part of the internal API.  Additional performance
+--   could be gained by using this internal API function.
+readOctet :: TextRead.Reader Word
+readOctet t = do
+  let (digits, rest) = Text.span Char.isDigit t
+  when (Text.null digits) $ Left "octet does not start with a digit"
+  case Text.foldr go Just digits 0 of
+    Just n  -> Right (fromIntegral n, rest)
+    Nothing -> Left ipOctetSizeErrorMsg
+  where
+  go :: Char -> (Int -> Maybe Int) -> Int -> Maybe Int
+  go !d !f !n =
+    let n' = n * 10 + Char.ord d - 48
+    in  if n' <= 255 then f n' else Nothing
 
 stripDecimal :: Text -> Either String Text
 stripDecimal t = case Text.uncons t of
diff --git a/src/Net/Mac.hs b/src/Net/Mac.hs
--- a/src/Net/Mac.hs
+++ b/src/Net/Mac.hs
@@ -103,6 +103,10 @@
 import qualified Data.Text.Short.Unsafe as TS
 import qualified Data.Text as Text ()
 
+#if MIN_VERSION_aeson(2,0,0)
+import qualified Data.Aeson.Key as AK
+#endif
+
 -- $setup
 --
 -- These are here to get doctest's property checking to work
@@ -858,8 +862,14 @@
 
 instance ToJSONKey Mac where
   toJSONKey = ToJSONKeyText
-    encode
+    (keyFromText . encode)
     (\m -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 m <> BB.char7 '"')
+    where
+#if MIN_VERSION_aeson(2,0,0)
+      keyFromText = AK.fromText
+#else
+      keyFromText = id
+#endif
 
 instance FromJSONKey Mac where
   fromJSONKey = FromJSONKeyTextParser $ \t -> case decode t of
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -140,6 +140,12 @@
       , PH.testCase "S" $ IPv4.reserved (IPv4.ipv4 192 88 99 0) @=? True
       , PH.testCase "T" $ IPv4.reserved (IPv4.ipv4 192 0 1 0) @=? False
       ]
+    , testGroup "private"
+      [ PH.testCase "A" $ IPv4.private (IPv4.ipv4 198 73 8 38) @=? False
+      , PH.testCase "B" $ IPv4.private (IPv4.ipv4 192 168 100 5) @=? True
+      , PH.testCase "C" $ IPv4.private (IPv4.ipv4 10 0 0 0) @=? True
+      , PH.testCase "D" $ IPv4.private (IPv4.ipv4 10 255 255 255) @=? True
+      ]
     ]
   , testGroup "IPv6 Range Operations"
     [ testProperty "Idempotence of normalizing IPv6 range"
@@ -413,6 +419,7 @@
   , "1.9.x.2"
   , "1.9.3"
   , "1.9"
+  , "127.0.0.18446744073709551617"
   ]
 
 testDecodeFailures :: [TestTree]
