diff --git a/enum-text.cabal b/enum-text.cabal
--- a/enum-text.cabal
+++ b/enum-text.cabal
@@ -1,13 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.31.2.
+-- This file has been generated from package.yaml by hpack version 0.34.4.
 --
 -- see: https://github.com/sol/hpack
---
--- hash: 89afd88fe91337c68085eb43ff907f8be6ba391eefe6883a46038dec6e671fde
 
 name:           enum-text
-version:        0.5.2.1
+version:        0.5.3.0
 synopsis:       A text rendering and parsing toolkit for enumerated types
 description:    A text rendering and parsing toolkit for enumerated types. Please see the README on GitHub at <https://github.com/cdornan/enum-text#readme>
 category:       Text
@@ -37,11 +35,38 @@
   ghc-options: -Wall
   build-depends:
       array
+    , attoparsec
     , base >=4.9 && <10
     , bytestring
     , fmt
     , hashable
     , possibly
+    , scientific
+    , text
+    , time
+    , unordered-containers
+  default-language: Haskell2010
+
+test-suite enum-text-tests
+  type: exitcode-stdio-1.0
+  main-is: Doctest.hs
+  other-modules:
+      Paths_enum_text
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      array
+    , attoparsec
+    , base >=4.9 && <10
+    , bytestring
+    , doctest
+    , doctest-discover
+    , enum-text
+    , fmt
+    , hashable
+    , possibly
+    , scientific
     , text
     , time
     , unordered-containers
diff --git a/src/Text/Enum/Text.hs b/src/Text/Enum/Text.hs
--- a/src/Text/Enum/Text.hs
+++ b/src/Text/Enum/Text.hs
@@ -13,12 +13,15 @@
   , defaultEnumTextConfig
   ) where
 
+import           Control.Monad                  (void)
 import           Control.Monad.Fail             as CMF
 import           Data.Array
+import qualified Data.Attoparsec.Text           as AP
 import qualified Data.ByteString.Char8          as B
 import           Data.Coerce
 import           Data.Hashable
 import           Data.Possibly
+import           Data.Scientific                (toBoundedInteger)
 import qualified Data.HashMap.Strict            as HM
 import qualified Data.Text                      as T
 import qualified Data.Text.Encoding             as TE
@@ -161,7 +164,22 @@
   parseText :: T.Text -> Possibly a
 
 instance TextParsable T.Text          where parseText = return
-instance TextParsable UTCTime         where parseText = parseTextRead "UTCTime"
+
+-- | Robust 'TextParsable' instance for 'UTCTime'.
+--
+-- Examples:
+--
+-- >>> show <$> parseText @UTCTime (T.pack "2020-01-01 10:12:30")
+-- Right "2020-01-01 10:12:30 UTC"
+--
+-- >>> show <$> parseText @UTCTime (T.pack "2020-01-01T10:12:30")
+-- Right "2020-01-01 10:12:30 UTC"
+--
+-- >>> show <$> parseText @UTCTime (T.pack "2020-01-01T10:12:30Z")
+-- Right "2020-01-01 10:12:30 UTC"
+--
+instance TextParsable UTCTime         where parseText = parseTextUsing parseUTC "UTCTime"
+
 instance TextParsable Day             where parseText = parseTextRead "Day"
 instance TextParsable Int             where parseText = parseDecimal
 instance a ~ Char => TextParsable [a] where parseText = return . T.unpack
@@ -234,14 +252,109 @@
       False -> Left $ "residual input: " ++ T.unpack r
 
 -- | Convert a 'Read' parser into a 'TextParsable'
+--
+-- Examples:
+--
+-- >>> parseTextRead @Int "Int" (T.pack "10")
+-- Right 10
+--
+-- >>> parseTextRead @Int "Int" (T.pack "a")
+-- Left "failed to parse Int: \"a\""
+--
 parseTextRead :: Read a
               => String   -- ^ name of type bing parsed (for failure message)
               -> T.Text   -- ^ 'T.Text' to be parsed
               -> Possibly a
-parseTextRead ty_s txt =
-    maybe (Left $ typeError ty_s $ show str) Right $ readMaybe str
+parseTextRead = parseTextUsing (readMaybe . T.unpack)
+
+-- | Use the given function to turn the input string into a 'TextParsable'
+--
+-- Examples:
+--
+-- >>> parseTextUsing @Int (readMaybe . T.unpack) "Int" (T.pack "10")
+-- Right 10
+--
+-- >>> parseTextUsing @Int (readMaybe . T.unpack) "Int" (T.pack "a")
+-- Left "failed to parse Int: \"a\""
+--
+parseTextUsing :: (T.Text -> Maybe a)
+               -> String   -- ^ name of type bing parsed (for failure message)
+               -> T.Text   -- ^ 'T.Text' to be parsed
+               -> Possibly a
+parseTextUsing maybeParser ty_s txt =
+    maybe (Left $ typeError ty_s $ show str) Right $ maybeParser txt
   where
     str = T.unpack txt
 
 typeError :: String -> String -> String
 typeError ty_s msg = "failed to parse "++ty_s++": "++msg
+
+-------------------------------------------------------------------------------
+-- support for parsing UTCTime (see enum-text#1)
+-------------------------------------------------------------------------------
+
+-- The following has been cribbed from the \"Data.API.Time\" module from the
+-- \"api-tools\" package.
+
+-- | Parse text as a 'UTCTime' in ISO 8601 format or a number of slight
+-- variations thereof (the @T@ may be replaced with a space, and the seconds,
+-- milliseconds and/or @Z@ timezone indicator may optionally be omitted).
+--
+-- Time zone designations other than @Z@ for UTC are not currently supported.
+parseUTC :: T.Text -> Maybe UTCTime
+parseUTC t = case AP.parseOnly (parserUTCTime <* AP.endOfInput) t of
+    Left _  -> Nothing
+    Right r -> Just r
+
+parserUTCTime :: AP.Parser UTCTime
+parserUTCTime = do
+    day <- parserDay
+    void $ AP.skip (\c -> c == ' ' || c == 'T')
+    time <- parserTime
+    mb_offset <- parserTimeZone
+    pure (maybe id addUTCTime mb_offset $ UTCTime day time)
+
+-- | Parser for @YYYY-MM-DD@ format.
+parserDay :: AP.Parser Day
+parserDay = do
+    y :: Int <- AP.decimal <* AP.char '-'
+    m :: Int <- AP.decimal <* AP.char '-'
+    d :: Int <- AP.decimal
+    case fromGregorianValid (fromIntegral y) m d of
+        Just x  -> pure x
+        Nothing -> fail "invalid date"
+
+-- | Parser for times in the format @HH:MM@, @HH:MM:SS@ or @HH:MM:SS.QQQ...@.
+parserTime :: AP.Parser DiffTime
+parserTime = do
+    h :: Int <- AP.decimal
+    void $ AP.char ':'
+    m :: Int <- AP.decimal
+    c <- AP.peekChar
+    s <- case c of
+           Just ':' -> AP.anyChar *> AP.scientific
+           _        -> pure 0
+    case toBoundedInteger (10^(12::Int) * (s + fromIntegral (60*(m + 60*h)))) of
+      Just n -> pure (picosecondsToDiffTime (fromIntegral (n :: Int)))
+      Nothing -> fail "seconds out of range"
+
+-- | Parser for time zone indications such as @Z@, @ UTC@ or an explicit offset
+-- like @+HH:MM@ or @-HH@.  Returns 'Nothing' for UTC.  Local times (without a
+-- timezone designator) are assumed to be UTC.  If there is an explicit offset,
+-- returns its negation.
+parserTimeZone :: AP.Parser (Maybe NominalDiffTime)
+parserTimeZone = do
+    c <- AP.option 'Z' AP.anyChar
+    case c of
+      'Z' -> pure Nothing
+      ' ' -> "UTC" *> pure Nothing
+      '+' -> parse_offset True
+      '-' -> parse_offset False
+      _   -> fail "unexpected time zone character"
+  where
+    parse_offset pos = do
+      hh :: Int <- read <$> AP.count 2 AP.digit
+      AP.option () (AP.skip (== ':'))
+      mm :: Int <- AP.option 0 (read <$> AP.count 2 AP.digit)
+      let v = (if pos then negate else id) ((hh*60 + mm) * 60)
+      return (Just (fromIntegral v))
diff --git a/test/Doctest.hs b/test/Doctest.hs
new file mode 100644
--- /dev/null
+++ b/test/Doctest.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF doctest-discover -optF test/doctest-config.json #-}
