diff --git a/pa-field-parser.cabal b/pa-field-parser.cabal
--- a/pa-field-parser.cabal
+++ b/pa-field-parser.cabal
@@ -1,7 +1,7 @@
 cabal-version:      3.0
 -- !!! ATTN: file autogenerated from pa-template.cabal.mustache on toplevel !!!
 name:               pa-field-parser
-version:            0.1.0.1
+version:            0.2.0.0
 synopsis:           “Vertical” parsing of values
 description:        
 license:            BSD-3-Clause
@@ -74,12 +74,14 @@
       base <5,
       pa-prelude,
       pa-error-tree,
-      containers,
-      scientific,
-      semigroupoids,
-      aeson,
       aeson-better-errors,
+      aeson,
       attoparsec,
       case-insensitive,
+      containers,
+      scientific,
+      semigroupoids,
+      template-haskell,
       text,
+      time,
 
diff --git a/src/FieldParser.hs b/src/FieldParser.hs
--- a/src/FieldParser.hs
+++ b/src/FieldParser.hs
@@ -1,5 +1,7 @@
 {-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskellQuotes #-}
 {-# LANGUAGE TypeFamilies #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
 
 module FieldParser where
 
@@ -12,6 +14,7 @@
 import Data.Attoparsec.Text qualified as Atto
 import Data.CaseInsensitive qualified as CaseInsensitive
 import Data.Error.Tree
+import Data.Fixed qualified as Fixed
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Map.Strict qualified as Map
 import Data.Scientific (Scientific)
@@ -19,8 +22,13 @@
 import Data.Semigroup.Foldable (Foldable1 (toNonEmpty))
 import Data.Semigroupoid qualified as Semigroupoid
 import Data.Text qualified as Text
+import Data.Time qualified as Time
+import Data.Time.Format.ISO8601 qualified as Time.Format.ISO
+import Language.Haskell.TH qualified as TH
+import Language.Haskell.TH.Syntax qualified as TH
 import PossehlAnalyticsPrelude
 import Text.ParserCombinators.ReadPrec qualified as Read
+import Prelude hiding (or)
 
 -- | Parser for a field. TODO: define what a field is
 --
@@ -145,6 +153,12 @@
   Left _err -> Left $ "Not a valid UTF-8 string"
   Right a -> Right a
 
+-- | Assert that the string is not empty
+notEmptyStringP :: FieldParser Text Text
+notEmptyStringP = FieldParser $ \case
+  "" -> Left [fmt|String cannot be empty|]
+  t -> Right t
+
 -- | A decimal number with an optional `+` or `-` sign character.
 signedDecimal :: FieldParser Text Integer
 signedDecimal =
@@ -241,6 +255,34 @@
     iMinBound = toInteger (minBound :: i)
     iMaxBound = toInteger (maxBound :: i)
 
+-- | ex: @2021-02-23@
+hyphenatedDay :: FieldParser Text Time.Day
+hyphenatedDay =
+  FieldParser $ \t ->
+    case parseDay t of
+      Nothing -> Left $ [fmt|Not a valid date of format yyyy-mm-dd: "{t}"|]
+      Just day -> Right day
+  where
+    parseDay :: Text -> Maybe Time.Day
+    parseDay t =
+      t
+        & textToString
+        & Time.Format.ISO.iso8601ParseM @Maybe @Time.Day
+
+-- | @yyyy-mm-ddThh:mm:ss[.sss]Z@ (ISO 8601:2004(E) sec. 4.3.2 extended format)
+utcTime :: FieldParser Text Time.UTCTime
+utcTime =
+  FieldParser $ \t ->
+    case parseTime t of
+      Nothing -> Left $ [fmt|Not a valid date of format `yyyy-mm-ddThh:mm:ss[.sss]Z` (ISO 8601:2004(E) sec. 4.3.2 extended format): "{t}"|]
+      Just day -> Right day
+  where
+    parseTime :: Text -> Maybe Time.UTCTime
+    parseTime t =
+      t
+        & textToString
+        & Time.Format.ISO.iso8601ParseM @Maybe @Time.UTCTime
+
 -- | Example of how to create a more “complicated” parser that checks whether a value
 -- is between two other values.
 clamped ::
@@ -323,6 +365,16 @@
     flipEither (Left err) = Right err
     flipEither (Right a) = Left a
 
+-- | Parse into Nothing if the Monoid (e.g. Text, Map etc.) was empty
+emptyOr :: forall s a. (Eq s, Show s, Monoid s) => FieldParser s a -> FieldParser' Error s (Maybe a)
+emptyOr inner =
+  FieldParser $ \from ->
+    if from == mempty
+      then Right Nothing
+      else case runFieldParser inner from of
+        Left err -> Left $ errorContext [fmt|Value was neither empty ("{mempty @s & show}") nor|] err
+        Right a -> Right (Just a)
+
 -- | Given a pretty printing function, it will create a parser
 -- that uses the inverse function to parse the field.
 --
@@ -472,3 +524,34 @@
    in FieldParser $ \bytes -> case parseAll bytes of
         Left _attoErr -> Left err
         Right a -> Right a
+
+-- | Parse a literal value at compile time. This is used with Template Haskell, like so:
+--
+-- > $$("2023-07-27" & literal hyphenatedDay) :: Time.Day
+--
+-- You need the double @$$@!
+--
+-- ATTN: This needs an instance of the 'TH.Lift' class for the output type.
+-- Many library types don’t yet implement this class, so we have to provide the instances ourselves.
+-- See NOTE: Lift for library types
+literal :: forall from to. TH.Lift to => FieldParser from to -> from -> TH.Code TH.Q to
+literal parser s = do
+  case runFieldParser parser s of
+    Right a -> [||a||]
+    Left err -> TH.liftCode (err & prettyError & textToString & fail)
+
+{-
+NOTE: Lift for library types
+
+Newer versions of Template Haskell provide a `Lift` class,
+which lets us parse types from e.g. strings at compile time.
+
+But many types do not implement the stock `Lift` instance yet
+(it can be auto-derived by GHC if requested).
+-}
+
+deriving stock instance (TH.Lift Fixed.Pico)
+
+deriving stock instance (TH.Lift Time.TimeOfDay)
+
+deriving stock instance (TH.Lift Time.Day)
