diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Revision history for chronos
 
+## 1.1.7.0 -- 2025.07.15
+
+* Add `decodeIso8601Zulu` for going straight from `Text` to a `Datetime`.
+* Require a newer version of the `text` library so that UTF-8 encoding is used.
+
 ## 1.1.6.2 -- 2024-12-11
 
 * Support GHC 9.10
@@ -10,7 +15,11 @@
 
 ## 1.1.6.0 -- 2024-01-29
 
-* Add `sinceEpoch` and `asSeconds` functions
+* Add `sinceEpoch` and `asSeconds` functions.
+* Make `boundedBuilderUtf8BytesIso8601Zoneless` suppress trailing zeros
+  when a timestamp has no submillisecond part. For example, this function
+  prints 2021-01-05T23:00:52.123 where it previously
+  printed 2021-01-05T23:00:52.123000000.
 
 ## 1.1.5.1 -- 2023-08-24
 
diff --git a/chronos.cabal b/chronos.cabal
--- a/chronos.cabal
+++ b/chronos.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.0
 name:            chronos
-version:         1.1.6.2
+version:         1.1.7.0
 synopsis:        A high-performance time library
 description:
   Chronos is a performance-oriented time library for Haskell, with a
@@ -24,7 +24,6 @@
   format strings. The approach taken by Chronos is faster and
   catches more mistakes at compile time, at the cost of being
   less expressive.
-
 homepage:        https://github.com/byteverse/chronos
 bug-reports:     https://github.com/byteverse/chronos/issues
 license:         BSD-3-Clause
@@ -38,7 +37,7 @@
   CHANGELOG.md
   README.md
 
-tested-with:     GHC ==9.4.8 || ==9.6.3 || ==9.8.1
+tested-with:     GHC ==9.6.3 || ==9.8.1
 
 common build-settings
   default-language: Haskell2010
@@ -52,11 +51,8 @@
     Chronos.Internal.CTimespec
     Chronos.Locale.English
     Chronos.Types
-
-  -- only exposed for doctests
-  -- it is OPTIONS_HADDOCK-hidden
   build-depends:
-    , aeson               >=1.1     && <2.3
+    , aeson               >=2.2     && <2.3
     , attoparsec          >=0.13    && <0.15
     , base                >=4.14    && <5
     , bytebuild           >=0.3.14  && <0.4
@@ -67,11 +63,10 @@
     , hashable            >=1.2     && <1.5
     , natural-arithmetic  >=0.1.2   && <0.3
     , primitive           >=0.6.4   && <0.10
-    , text                >=1.2     && <1.3  || >=2.0 && <2.2
-    , text-short          >=0.1.3   && <0.2
+    , text                >=2.1.2   && <2.2
+    , text-short          >=0.1.5   && <0.2
     , torsor              >=0.1     && <0.2
     , vector              >=0.11    && <0.14
-
   if os(windows)
     build-depends: Win32 >=2.2 && <2.14
 
@@ -85,12 +80,12 @@
   main-is:        Spec.hs
   build-depends:
     , aeson                       >=1.1 && <2.3
+    , HUnit
+    , QuickCheck
     , attoparsec
     , base
     , bytestring
     , chronos
-    , HUnit
-    , QuickCheck
     , test-framework
     , test-framework-hunit
     , test-framework-quickcheck2
diff --git a/src/Chronos.hs b/src/Chronos.hs
--- a/src/Chronos.hs
+++ b/src/Chronos.hs
@@ -207,6 +207,7 @@
   , decode_DmyHMS_opt_S
   , decode_DmyHMS_opt_S_lenient
   , decode_lenient
+  , decodeIso8601Zulu
 
     -- *** UTF-8 ByteString
   , encodeUtf8_YmdHMS
@@ -351,57 +352,60 @@
   , _timeOfDayNanoseconds
   ) where
 
-import qualified Arithmetic.Lte as Lte
-import qualified Arithmetic.Nat as Nat
 import Control.Applicative
 import Control.DeepSeq (NFData (..), deepseq)
 import Control.Exception (evaluate)
 import Control.Monad
 import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
+import Data.Attoparsec.Text (Parser)
+import Data.Bool (bool)
+import Data.ByteString (ByteString)
+import Data.Bytes (Bytes)
+import Data.Char (isDigit)
+import Data.Foldable
+import Data.Hashable (Hashable)
+import Data.Int (Int64)
+import Data.Primitive
+import Data.Text (Text)
+import Data.Text.Short (ShortText)
+import Data.Vector (Vector)
+import Data.Word (Word64, Word8)
+import Foreign.Storable
+import GHC.Clock (getMonotonicTimeNSec)
+import GHC.Generics (Generic)
+import Torsor
+
+import qualified Arithmetic.Lte as Lte
+import qualified Arithmetic.Nat as Nat
 import qualified Data.Aeson as AE
 import qualified Data.Aeson.Encoding as AEE
+import qualified Data.Aeson.Key as AK
 import qualified Data.Aeson.Types as AET
 import qualified Data.Attoparsec.ByteString.Char8 as AB
-import Data.Attoparsec.Text (Parser)
 import qualified Data.Attoparsec.Text as AT
 import qualified Data.Attoparsec.Zepto as Z
-import Data.Bool (bool)
-import Data.ByteString (ByteString)
 import qualified Data.ByteString.Builder as BB
 import qualified Data.ByteString.Char8 as BC
 import qualified Data.ByteString.Lazy as LB
 import qualified Data.ByteString.Short.Internal as SBS
-import Data.Bytes (Bytes)
 import qualified Data.Bytes as Bytes
 import qualified Data.Bytes.Builder.Bounded as Bounded
 import qualified Data.Bytes.Parser as BVP
 import qualified Data.Bytes.Parser.Latin as Latin
-import Data.Char (isDigit)
-import Data.Foldable
-import Data.Hashable (Hashable)
-import Data.Int (Int64)
-import Data.Primitive
+import qualified Data.Bytes.Text.Utf8 as Utf8
 import qualified Data.Semigroup as SG
-import Data.Text (Text)
 import qualified Data.Text as Text
 import qualified Data.Text.Lazy as LT
 import qualified Data.Text.Lazy.Builder as TB
 import qualified Data.Text.Lazy.Builder.Int as TB
 import qualified Data.Text.Read as Text
-import Data.Text.Short (ShortText)
 import qualified Data.Text.Short as TS
 import qualified Data.Text.Short.Unsafe as TS
-import Data.Vector (Vector)
 import qualified Data.Vector as Vector
 import qualified Data.Vector.Generic as GVector
 import qualified Data.Vector.Generic.Mutable as MGVector
 import qualified Data.Vector.Primitive as PVector
 import qualified Data.Vector.Unboxed as UVector
-import Data.Word (Word64, Word8)
-import Foreign.Storable
-import GHC.Clock (getMonotonicTimeNSec)
-import GHC.Generics (Generic)
-import Torsor
 
 #ifdef mingw32_HOST_OS
 import System.Win32.Time (SYSTEMTIME(..))
@@ -410,10 +414,6 @@
 import Chronos.Internal.CTimespec (getPosixNanoseconds)
 #endif
 
-#if MIN_VERSION_aeson(2,0,0)
-import qualified Data.Aeson.Key as AK
-#endif
-
 {- $setup
 >>> import Test.QuickCheck hiding (within)
 >>> import Test.QuickCheck.Gen
@@ -1461,7 +1461,7 @@
                         else quot x (raiseTenTo (totalDigits - 9))
             pure (fromIntegral result)
           _ -> pure 0
-      )
+    )
       <|> pure 0
   pure (s * 1000000000 + nanoseconds)
 
@@ -2139,7 +2139,7 @@
                         else quot x (raiseTenTo (totalDigits - 9))
             pure (fromIntegral result)
           _ -> pure 0
-      )
+    )
       <|> pure 0
   pure (s * 1000000000 + nanoseconds)
 
@@ -2835,7 +2835,7 @@
                     then x * raiseTenTo (9 - totalDigits)
                     else quot x (raiseTenTo (totalDigits - 9))
         pure (fromIntegral result)
-      )
+    )
       <|> pure 0
   pure (s * 1000000000 + nanoseconds)
 
@@ -3792,20 +3792,30 @@
 {- | Decode an ISO-8601-encode datetime. The encoded time must be suffixed
 by either @Z@ or @+00:00@ or @+00@.
 -}
+decodeIso8601Zulu :: Text -> Maybe Chronos.Datetime
+{-# INLINE decodeIso8601Zulu #-}
+decodeIso8601Zulu !t =
+  BVP.parseBytesMaybe parserIso8601Zulu (Utf8.fromText t)
+
+{- | Decode an ISO-8601-encode datetime. The encoded time must be suffixed
+by either @Z@ or @+00:00@ or @+00@.
+-}
 decodeShortTextIso8601Zulu :: ShortText -> Maybe Chronos.Datetime
+{-# INLINE decodeShortTextIso8601Zulu #-}
 decodeShortTextIso8601Zulu !t =
-  BVP.parseBytesMaybe
-    ( do
-        d <- parserUtf8BytesIso8601Zoneless 'T'
-        remaining <- BVP.remaining
-        case Bytes.length remaining of
-          1 | Bytes.unsafeIndex remaining 0 == 0x5A -> pure d
-          3 | Bytes.equalsCString (Ptr "+00"#) remaining -> pure d
-          6 | Bytes.equalsCString (Ptr "+00:00"#) remaining -> pure d
-          _ -> BVP.fail ()
-    )
-    (Bytes.fromShortByteString (TS.toShortByteString t))
+  BVP.parseBytesMaybe parserIso8601Zulu (Utf8.fromShortText t)
 
+parserIso8601Zulu :: BVP.Parser () s Chronos.Datetime
+{-# NOINLINE parserIso8601Zulu #-}
+parserIso8601Zulu = do
+  d <- parserUtf8BytesIso8601Zoneless 'T'
+  remaining <- BVP.remaining
+  case Bytes.length remaining of
+    1 | Bytes.unsafeIndex remaining 0 == 0x5A -> pure d
+    3 | Bytes.equalsCString (Ptr "+00"#) remaining -> pure d
+    6 | Bytes.equalsCString (Ptr "+00:00"#) remaining -> pure d
+    _ -> BVP.fail ()
+
 {- | Decode an ISO-8601-encode datetime. The encoded time must not be suffixed
 by an offset. Any offset (e.g. @-05:00@, @+00:00@, @Z@) will cause a decode
 failure.
@@ -3956,7 +3966,7 @@
 Examples of output:
 
 > 2021-01-05T23:00:51
-> 2021-01-05T23:00:52.123000000
+> 2021-01-05T23:00:52.123
 > 2021-01-05T23:00:53.674094347
 -}
 boundedBuilderUtf8BytesIso8601Zoneless :: Datetime -> Bounded.Builder 44
@@ -3974,10 +3984,19 @@
         `Bounded.append` Bounded.ascii ':'
         `Bounded.append` Bounded.wordPaddedDec2 (fromIntegral s)
         `Bounded.append` ( case ns of
-                            0 -> Bounded.weaken @0 @10 Lte.constant Bounded.empty
-                            _ ->
-                              Bounded.ascii '.'
-                                `Bounded.append` Bounded.wordPaddedDec9 (fromIntegral ns)
+                             0 -> Bounded.weaken @0 @10 Lte.constant Bounded.empty
+                             _ ->
+                               let (ms, us) = quotRem ns 1_000_000
+                                in case us of
+                                     0 ->
+                                       Bounded.weaken @4 @10
+                                         Lte.constant
+                                         ( Bounded.ascii '.'
+                                             `Bounded.append` Bounded.wordPaddedDec3 (fromIntegral ms)
+                                         )
+                                     _ ->
+                                       Bounded.ascii '.'
+                                         `Bounded.append` Bounded.wordPaddedDec9 (fromIntegral ns)
                          )
 
 boundedBuilderOffset :: Offset -> Bounded.Builder 6
