diff --git a/README.lhs b/README.lhs
--- a/README.lhs
+++ b/README.lhs
@@ -13,7 +13,6 @@
 
 module Main (main) where
 import Prelude
-import Text.Markdown.Unlit ()
 
 -- Used in a later example
 import qualified Test.Hspec.JUnit.Formatter.Env as FormatterEnv
@@ -119,6 +118,20 @@
 We maintain specific golden XML files for GHC 8.x vs 9.x, so you will need to
 re-run the test suite with at least one of each series to regenerate all the
 necessary files.
+
+## Release
+
+To trigger a release, merge a commit to `main` that follows [Conventional
+Commits][]. In short,
+
+- `fix:` to trigger a patch release
+- `feat:` to trigger a minor release
+- `<type>!:` or use a `BREAKING CHANGE:` footer to trigger a major release
+
+We don't enforce conventional commits generally (though you are free do so),
+it's only required if you want to trigger release.
+
+[conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/#summary
 
 ---
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@
 
 module Main (main) where
 import Prelude
-import Text.Markdown.Unlit ()
 
 -- Used in a later example
 import qualified Test.Hspec.JUnit.Formatter.Env as FormatterEnv
@@ -119,6 +118,20 @@
 We maintain specific golden XML files for GHC 8.x vs 9.x, so you will need to
 re-run the test suite with at least one of each series to regenerate all the
 necessary files.
+
+## Release
+
+To trigger a release, merge a commit to `main` that follows [Conventional
+Commits][]. In short,
+
+- `fix:` to trigger a patch release
+- `feat:` to trigger a minor release
+- `<type>!:` or use a `BREAKING CHANGE:` footer to trigger a major release
+
+We don't enforce conventional commits generally (though you are free do so),
+it's only required if you want to trigger release.
+
+[conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/#summary
 
 ---
 
diff --git a/hspec-junit-formatter.cabal b/hspec-junit-formatter.cabal
--- a/hspec-junit-formatter.cabal
+++ b/hspec-junit-formatter.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               hspec-junit-formatter
-version:            1.1.2.1
+version:            1.2.0.0
 license:            MIT
 license-file:       LICENSE
 copyright:          2021 Renaissance Learning Inc
@@ -30,7 +30,6 @@
 
 library
     exposed-modules:
-        Test.Hspec.JUnit
         Test.Hspec.JUnit.Config
         Test.Hspec.JUnit.Config.Env
         Test.Hspec.JUnit.Format
@@ -41,87 +40,75 @@
 
     hs-source-dirs:     library
     other-modules:      Paths_hspec_junit_formatter
-    default-language:   Haskell2010
+    default-language:   GHC2021
     default-extensions:
-        BangPatterns DeriveAnyClass DeriveFoldable DeriveFunctor
-        DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies
-        FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving
-        LambdaCase MultiParamTypeClasses NoImplicitPrelude
-        NoMonomorphismRestriction OverloadedStrings RankNTypes
-        RecordWildCards ScopedTypeVariables StandaloneDeriving
-        TypeApplications TypeFamilies
+        DataKinds DeriveAnyClass DerivingStrategies DerivingVia
+        DuplicateRecordFields GADTs LambdaCase NoFieldSelectors
+        NoImplicitPrelude NoMonomorphismRestriction NoPostfixOperators
+        OverloadedRecordDot OverloadedStrings QuasiQuotes TypeFamilies
 
     ghc-options:
-        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations
+        -Weverything -Wno-all-missed-specialisations
         -Wno-missed-specialisations -Wno-missing-exported-signatures
         -Wno-missing-import-lists -Wno-missing-local-signatures
-        -Wno-monomorphism-restriction -Wno-safe -Wno-unsafe
+        -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction
+        -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe
 
     build-depends:
-        array >=0.5.2.0,
-        base >=4.11.1.0 && <5,
-        conduit >=1.3.1,
-        containers >=0.5.11.0,
-        directory >=1.3.1.5,
-        exceptions >=0.10.0,
-        filepath >=1.4.2,
+        array >=0.5.4.0,
+        base >=4.16.4.0 && <5,
+        conduit >=1.3.5,
+        containers >=0.6.5.1,
+        directory >=1.3.6.2,
+        exceptions >=0.10.4,
+        filepath >=1.4.2.2,
         hspec-api >=2.10.0,
         hspec-core >=2.10.0,
         iso8601-time >=0.1.5,
         regex-base >=0.94.0.2,
-        regex-tdfa >=1.3.2.2,
-        text >=1.2.3.1,
-        time >=1.8.0.2,
-        xml-conduit >=1.8.0.1,
-        xml-types >=0.3.6
+        regex-tdfa >=1.3.2.1,
+        text >=1.2.5.0,
+        time >=1.11.1.1,
+        xml-conduit >=1.9.1.2,
+        xml-types >=0.3.8
 
+    if impl(ghc >=9.8)
+        ghc-options: -Wno-missing-role-annotations
+
     if impl(ghc >=9.2)
         ghc-options: -Wno-missing-kind-signatures
 
-    if impl(ghc >=8.10)
-        ghc-options:
-            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module
-
-    if impl(ghc >=8.8)
-        ghc-options: -fwrite-ide-info
-
 test-suite readme
     type:               exitcode-stdio-1.0
     main-is:            README.lhs
+    build-tool-depends: markdown-unlit:markdown-unlit
     other-modules:      Paths_hspec_junit_formatter
-    default-language:   Haskell2010
+    default-language:   GHC2021
     default-extensions:
-        BangPatterns DeriveAnyClass DeriveFoldable DeriveFunctor
-        DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies
-        FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving
-        LambdaCase MultiParamTypeClasses NoImplicitPrelude
-        NoMonomorphismRestriction OverloadedStrings RankNTypes
-        RecordWildCards ScopedTypeVariables StandaloneDeriving
-        TypeApplications TypeFamilies
+        DataKinds DeriveAnyClass DerivingStrategies DerivingVia
+        DuplicateRecordFields GADTs LambdaCase NoFieldSelectors
+        NoImplicitPrelude NoMonomorphismRestriction NoPostfixOperators
+        OverloadedRecordDot OverloadedStrings QuasiQuotes TypeFamilies
 
     ghc-options:
-        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations
+        -Weverything -Wno-all-missed-specialisations
         -Wno-missed-specialisations -Wno-missing-exported-signatures
         -Wno-missing-import-lists -Wno-missing-local-signatures
-        -Wno-monomorphism-restriction -Wno-safe -Wno-unsafe -pgmL
+        -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction
+        -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe -pgmL
         markdown-unlit
 
     build-depends:
-        base >=4.11.1.0 && <5,
+        base >=4.16.4.0 && <5,
         hspec >=2.10.0,
-        hspec-junit-formatter,
-        markdown-unlit >=0.5.0
+        hspec-junit-formatter
 
+    if impl(ghc >=9.8)
+        ghc-options: -Wno-missing-role-annotations
+
     if impl(ghc >=9.2)
         ghc-options: -Wno-missing-kind-signatures
 
-    if impl(ghc >=8.10)
-        ghc-options:
-            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module
-
-    if impl(ghc >=8.8)
-        ghc-options: -fwrite-ide-info
-
 test-suite spec
     type:               exitcode-stdio-1.0
     main-is:            Spec.hs
@@ -133,40 +120,34 @@
         Test.Hspec.JUnit.FormatterSpec
         Paths_hspec_junit_formatter
 
-    default-language:   Haskell2010
+    default-language:   GHC2021
     default-extensions:
-        BangPatterns DeriveAnyClass DeriveFoldable DeriveFunctor
-        DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies
-        FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving
-        LambdaCase MultiParamTypeClasses NoImplicitPrelude
-        NoMonomorphismRestriction OverloadedStrings RankNTypes
-        RecordWildCards ScopedTypeVariables StandaloneDeriving
-        TypeApplications TypeFamilies
+        DataKinds DeriveAnyClass DerivingStrategies DerivingVia
+        DuplicateRecordFields GADTs LambdaCase NoFieldSelectors
+        NoImplicitPrelude NoMonomorphismRestriction NoPostfixOperators
+        OverloadedRecordDot OverloadedStrings QuasiQuotes TypeFamilies
 
     ghc-options:
-        -fignore-optim-changes -Weverything -Wno-all-missed-specialisations
+        -Weverything -Wno-all-missed-specialisations
         -Wno-missed-specialisations -Wno-missing-exported-signatures
         -Wno-missing-import-lists -Wno-missing-local-signatures
-        -Wno-monomorphism-restriction -Wno-safe -Wno-unsafe -threaded
-        -rtsopts -O0 -with-rtsopts=-N
+        -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction
+        -Wno-prepositive-qualified-module -Wno-safe -Wno-unsafe -threaded
+        -rtsopts -with-rtsopts=-N
 
     build-depends:
-        base >=4.11.1.0 && <5,
-        containers >=0.5.11.0,
-        filepath >=1.4.2,
+        base >=4.16.4.0 && <5,
+        containers >=0.6.5.1,
+        filepath >=1.4.2.2,
         hspec >=2.10.0,
         hspec-golden >=0.2.1.0,
         hspec-junit-formatter,
         temporary >=1.3,
-        text >=1.2.3.1,
-        xml-conduit >=1.8.0.1
+        text >=1.2.5.0,
+        xml-conduit >=1.9.1.2
 
+    if impl(ghc >=9.8)
+        ghc-options: -Wno-missing-role-annotations
+
     if impl(ghc >=9.2)
         ghc-options: -Wno-missing-kind-signatures
-
-    if impl(ghc >=8.10)
-        ghc-options:
-            -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module
-
-    if impl(ghc >=8.8)
-        ghc-options: -fwrite-ide-info
diff --git a/library/Test/Hspec/JUnit.hs b/library/Test/Hspec/JUnit.hs
deleted file mode 100644
--- a/library/Test/Hspec/JUnit.hs
+++ /dev/null
@@ -1,226 +0,0 @@
-{-# LANGUAGE CPP #-}
-
-module Test.Hspec.JUnit
-  {-# DEPRECATED "Use Test.Hspec.JUnit.Formatter" #-}
-  ( -- * Runners
-    hspecJUnit
-  , hspecJUnitWith
-
-    -- * Directly modifying 'Config'
-  , configWithJUnit
-  , configWithJUnitAvailable
-
-    -- * Actual format function
-  , junitFormat
-
-    -- * Configuration
-  , module Test.Hspec.JUnit.Config
-  ) where
-
-import Prelude
-
-import Control.Applicative ((<|>))
-import Data.Conduit (runConduitRes, (.|))
-import Data.Conduit.Combinators (sinkFile)
-import Data.Conduit.List (sourceList)
-import Data.Functor ((<&>))
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Data.Text (Text, pack, unpack)
-import qualified Data.Text as T
-import Data.Time (getCurrentTime)
-import System.Directory (createDirectoryIfMissing)
-import System.FilePath (splitFileName)
-import Test.Hspec.Core.Format
-  ( Event (..)
-  , FailureReason (..)
-  , Format
-  , FormatConfig
-  , Item (..)
-  , Location (..)
-  , Path
-  , Result (..)
-  , Seconds (..)
-  )
-import Test.Hspec.Core.Runner (Config (..), defaultConfig, hspecWith)
-import Test.Hspec.Core.Spec (Spec)
-import Test.Hspec.JUnit.Config
-import Test.Hspec.JUnit.Config.Env
-import Test.Hspec.JUnit.Render (renderJUnit)
-import qualified Test.Hspec.JUnit.Schema as Schema
-import Text.XML.Stream.Render (def, renderBytes)
-
--- | Like 'hspec' but adds JUNit functionality
---
--- To actually /use/ the JUnit format, you must set @JUNIT_ENABLED=1@ in the
--- environment; by default, this function just behaves like 'hspec'.
---
--- Running tests with @--test-arguments="-f junit"@ also works.
---
--- All configuration of the JUnit report occurs through environment variables.
---
--- See "Test.Hspec.JUnit.Config" and "Test.Hspec.JUnit.Config.Env".
-hspecJUnit :: Spec -> IO ()
-hspecJUnit = hspecJUnitWith defaultConfig
-
--- | 'hspecJUnit' but built on a non-default 'Config'
-hspecJUnitWith :: Config -> Spec -> IO ()
-hspecJUnitWith config spec = do
-  junitEnabled <- envJUnitEnabled
-  junitConfig <- envJUnitConfig
-
-  let
-    modify = if junitEnabled then configWithJUnit junitConfig else id
-    base = configWithJUnitAvailable junitConfig config
-
-  hspecWith (modify base) spec
-
--- | Modify an Hspec 'Config' to use 'junitFormat'
-configWithJUnit :: JUnitConfig -> Config -> Config
-configWithJUnit junitConfig config =
-  config {configFormat = Just $ junitFormat junitConfig}
-
--- | Modify an Hspec 'Config' to have the 'junitFormat' /available/
---
--- Adds @junit@ to the list of available options for @-f, --format@.
-configWithJUnitAvailable :: JUnitConfig -> Config -> Config
-configWithJUnitAvailable junitConfig config =
-  config
-    { configAvailableFormatters =
-        configAvailableFormatters config <> [("junit", junitFormat junitConfig)]
-    }
-
--- | Hspec 'configFormat' that generates a JUnit report
-junitFormat :: JUnitConfig -> FormatConfig -> IO Format
-junitFormat junitConfig _config = pure $ \case
-  Started -> pure ()
-  GroupStarted _ -> pure ()
-  GroupDone _ -> pure ()
-  Progress _ _ -> pure ()
-  ItemStarted _ -> pure ()
-  ItemDone _ _ -> pure ()
-  Done paths -> do
-    time <- getCurrentTime
-
-    let (directory, _) = splitFileName file
-    createDirectoryIfMissing True directory
-
-    let
-      groups = groupItems paths
-      output =
-        Schema.Suites
-          { suitesName = suiteName
-          , suitesSuites =
-              groups <&> \(group, items) -> do
-                let suite xs =
-                      Schema.Suite
-                        { suiteName = group
-                        , suiteTimestamp = time
-                        , suiteCases = xs
-                        }
-                suite $ uncurry (itemToTestCase applyPrefix group) <$> items
-          }
-
-    runConduitRes $
-      sourceList [output]
-        .| renderJUnit dropConsoleFormatting
-        .| renderBytes def
-        .| sinkFile file
- where
-  file = getJUnitConfigOutputFile junitConfig
-  suiteName = getJUnitConfigSuiteName junitConfig
-  applyPrefix = getJUnitPrefixSourcePath junitConfig
-  dropConsoleFormatting = getJUnitConfigDropConsoleFormatting junitConfig
-
-groupItems :: [(Path, Item)] -> [(Text, [(Text, Item)])]
-groupItems = Map.toList . Map.fromListWith (<>) . fmap group
- where
-  group ((path, name), item) =
-    (T.intercalate "/" $ pack <$> path, [(pack name, item)])
-
-itemToTestCase
-  :: (FilePath -> FilePath) -> Text -> Text -> Item -> Schema.TestCase
-itemToTestCase applyPrefix group name item =
-  Schema.TestCase
-    { testCaseLocation =
-        toSchemaLocation applyPrefix
-          <$> (itemResultLocation item <|> itemLocation item)
-    , testCaseClassName = group
-    , testCaseName = name
-    , testCaseDuration = unSeconds $ itemDuration item
-    , testCaseResult = case itemResult item of
-        Success -> Nothing
-        Pending mLocation mMessage ->
-          Just $
-            Schema.Skipped $
-              prefixLocation mLocation $
-                prefixInfo $
-                  maybe
-                    ""
-                    pack
-                    mMessage
-        Failure mLocation reason ->
-          Just $
-            Schema.Failure "error" $
-              prefixLocation mLocation $
-                prefixInfo $
-                  reasonToText reason
-    }
- where
-  prefixLocation mLocation str = case mLocation of
-    Nothing -> str
-    Just Location {..} ->
-      mconcat
-        [ pack $ applyPrefix locationFile
-        , ":"
-        , pack $ show locationLine
-        , ":"
-        , pack $ show locationColumn
-        , "\n"
-        ]
-        <> str
-  prefixInfo str
-    | T.null $ T.strip $ pack $ itemInfo item = str
-    | otherwise = pack (itemInfo item) <> "\n\n" <> str
-
-itemResultLocation :: Item -> Maybe Location
-itemResultLocation item = case itemResult item of
-  Success -> Nothing
-  Pending mLocation _ -> mLocation
-  Failure mLocation _ -> mLocation
-
-toSchemaLocation :: (FilePath -> FilePath) -> Location -> Schema.Location
-toSchemaLocation applyPrefix Location {..} =
-  Schema.Location
-    { Schema.locationFile = applyPrefix locationFile
-    , Schema.locationLine = fromIntegral $ max 0 locationLine
-    }
-
-unSeconds :: Seconds -> Double
-unSeconds (Seconds x) = x
-
-foundLines :: Show a => Text -> a -> [String]
-foundLines msg found = case lines' of
-  [] -> []
-  first : rest ->
-    unpack (msg <> ": " <> first) : (unpack . (T.replicate 9 " " <>) <$> rest)
- where
-  lines' = T.lines . pack $ show found
-
-{- FOURMOLU_DISABLE -}
-reasonToText :: FailureReason -> Text
-reasonToText = \case
-  Error _ err -> pack $ show err
-  NoReason -> "no reason"
-  Reason err -> pack err
-#if MIN_VERSION_hspec_core(2,11,0)
-  ColorizedReason err -> pack err
-#endif
-  ExpectedButGot preface expected actual ->
-    T.unlines
-      $ pack
-      <$> fromMaybe "" preface
-      : (foundLines "expected" expected
-        <> foundLines " but got" actual
-        )
-{- FOURMOLU_ENABLE -}
diff --git a/library/Test/Hspec/JUnit/Config.hs b/library/Test/Hspec/JUnit/Config.hs
--- a/library/Test/Hspec/JUnit/Config.hs
+++ b/library/Test/Hspec/JUnit/Config.hs
@@ -24,12 +24,12 @@
 import System.FilePath ((</>))
 
 data JUnitConfig = JUnitConfig
-  { junitConfigOutputDirectory :: FilePath
-  , junitConfigOutputName :: FilePath
-  , junitConfigOutputFile :: Maybe FilePath
-  , junitConfigSuiteName :: Text
-  , junitConfigSourcePathPrefix :: Maybe FilePath
-  , junitConfigDropConsoleFormatting :: Bool
+  { outputDirectory :: FilePath
+  , outputName :: FilePath
+  , outputFile :: Maybe FilePath
+  , suiteName :: Text
+  , sourcePathPrefix :: Maybe FilePath
+  , dropConsoleFormatting :: Bool
   }
 
 -- | Construct a 'JUnitConfig' given a suite name
@@ -38,35 +38,34 @@
 defaultJUnitConfig :: Text -> JUnitConfig
 defaultJUnitConfig name =
   JUnitConfig
-    { junitConfigOutputDirectory = "."
-    , junitConfigOutputName = "junit.xml"
-    , junitConfigOutputFile = Nothing
-    , junitConfigSuiteName = name
-    , junitConfigSourcePathPrefix = Nothing
-    , junitConfigDropConsoleFormatting = False
+    { outputDirectory = "."
+    , outputName = "junit.xml"
+    , outputFile = Nothing
+    , suiteName = name
+    , sourcePathPrefix = Nothing
+    , dropConsoleFormatting = False
     }
 
 -- | Set the directory within which to generate the report
 --
 -- Default is current working directory.
 setJUnitConfigOutputDirectory :: FilePath -> JUnitConfig -> JUnitConfig
-setJUnitConfigOutputDirectory x config =
-  config {junitConfigOutputDirectory = x}
+setJUnitConfigOutputDirectory x config = config {outputDirectory = x}
 
 -- | Set the name for the generated report
 --
 -- Default is @junit.xml@.
 setJUnitConfigOutputName :: FilePath -> JUnitConfig -> JUnitConfig
-setJUnitConfigOutputName x config = config {junitConfigOutputName = x}
+setJUnitConfigOutputName x config = config {outputName = x}
 
 -- | Set the full path to the generated report
 --
 -- If given, the directory and name configurations are ignored.
 setJUnitConfigOutputFile :: FilePath -> JUnitConfig -> JUnitConfig
-setJUnitConfigOutputFile x config = config {junitConfigOutputFile = Just x}
+setJUnitConfigOutputFile x config = config {outputFile = Just x}
 
 setJUnitConfigSuiteName :: Text -> JUnitConfig -> JUnitConfig
-setJUnitConfigSuiteName x config = config {junitConfigSuiteName = x}
+setJUnitConfigSuiteName x config = config {suiteName = x}
 
 -- | Set a prefix to apply to source paths in the report
 --
@@ -74,8 +73,7 @@
 -- in a monorepository, and you need reported paths to be from the repository
 -- root.
 setJUnitConfigSourcePathPrefix :: FilePath -> JUnitConfig -> JUnitConfig
-setJUnitConfigSourcePathPrefix x config =
-  config {junitConfigSourcePathPrefix = Just x}
+setJUnitConfigSourcePathPrefix x config = config {sourcePathPrefix = Just x}
 
 -- | Set whether console formatting characters should be dropped from failure
 -- reports.
@@ -83,27 +81,23 @@
 -- Default is False. Most XML processors will fail to parse the XML if it
 -- contains the ANSI control characters used by console formatting.
 setJUnitConfigDropConsoleFormatting :: Bool -> JUnitConfig -> JUnitConfig
-setJUnitConfigDropConsoleFormatting x config = config {junitConfigDropConsoleFormatting = x}
+setJUnitConfigDropConsoleFormatting x config = config {dropConsoleFormatting = x}
 
 -- | Retrieve the full path to the generated report
 getJUnitConfigOutputFile :: JUnitConfig -> FilePath
-getJUnitConfigOutputFile JUnitConfig {..} =
-  fromMaybe
-    (junitConfigOutputDirectory </> junitConfigOutputName)
-    junitConfigOutputFile
+getJUnitConfigOutputFile c = fromMaybe (c.outputDirectory </> c.outputName) c.outputFile
 
 -- | Retrieve the suite name given on construction
 getJUnitConfigSuiteName :: JUnitConfig -> Text
-getJUnitConfigSuiteName = junitConfigSuiteName
+getJUnitConfigSuiteName c = c.suiteName
 
 -- | Retrieve the function to apply to reported source paths
 --
 -- Will be 'id' if no prefix configured.
 getJUnitPrefixSourcePath :: JUnitConfig -> FilePath -> FilePath
-getJUnitPrefixSourcePath JUnitConfig {..} =
-  maybe id (</>) junitConfigSourcePathPrefix
+getJUnitPrefixSourcePath c = maybe id (</>) c.sourcePathPrefix
 
 -- | Retrieve whether console formatting characters should be dropped from
 -- failure reports.
 getJUnitConfigDropConsoleFormatting :: JUnitConfig -> Bool
-getJUnitConfigDropConsoleFormatting JUnitConfig {..} = junitConfigDropConsoleFormatting
+getJUnitConfigDropConsoleFormatting c = c.dropConsoleFormatting
diff --git a/library/Test/Hspec/JUnit/Config/Env.hs b/library/Test/Hspec/JUnit/Config/Env.hs
--- a/library/Test/Hspec/JUnit/Config/Env.hs
+++ b/library/Test/Hspec/JUnit/Config/Env.hs
@@ -11,7 +11,7 @@
 
 import Data.Semigroup (Endo (..))
 import Data.Text (pack, unpack)
-import qualified Data.Text as T
+import Data.Text qualified as T
 import System.Directory (getCurrentDirectory)
 import System.Environment (getEnvironment, lookupEnv)
 import System.FilePath (takeBaseName)
@@ -42,16 +42,16 @@
 readJUnitConfig base env = modify $ defaultJUnitConfig $ pack base
  where
   modify =
-    appEndo $
-      foldMap
+    appEndo
+      $ foldMap
         Endo
         [ readEnv "OUTPUT_DIRECTORY" setJUnitConfigOutputDirectory
         , readEnv "OUTPUT_NAME" setJUnitConfigOutputName
         , readEnv "OUTPUT_FILE" setJUnitConfigOutputFile
         , readEnv "SUITE_NAME" $ setJUnitConfigSuiteName . pack
         , readEnv "SOURCE_PATH_PREFIX" setJUnitConfigSourcePathPrefix
-        , readEnv "DROP_CONSOLE_FORMATTING" $
-            setJUnitConfigDropConsoleFormatting . (== "1")
+        , readEnv "DROP_CONSOLE_FORMATTING"
+            $ setJUnitConfigDropConsoleFormatting . (== "1")
         ]
 
   readEnv name setter =
diff --git a/library/Test/Hspec/JUnit/Format.hs b/library/Test/Hspec/JUnit/Format.hs
--- a/library/Test/Hspec/JUnit/Format.hs
+++ b/library/Test/Hspec/JUnit/Format.hs
@@ -4,22 +4,20 @@
 
 import Prelude
 
+import Conduit (runConduitRes, sinkFile, yield, (.|))
 import Control.Applicative ((<|>))
-import Data.Conduit (runConduitRes, (.|))
-import Data.Conduit.Combinators (sinkFile)
-import Data.Conduit.List (sourceList)
 import Data.Functor ((<&>))
-import qualified Data.Map.Strict as Map
+import Data.Map.Strict qualified as Map
 import Data.Maybe (fromMaybe)
 import Data.Text (Text, pack, unpack)
-import qualified Data.Text as T
+import Data.Text qualified as T
 import Data.Time (getCurrentTime)
 import System.Directory (createDirectoryIfMissing)
 import System.FilePath (splitFileName)
 import Test.Hspec.Api.Format.V1
 import Test.Hspec.JUnit.Config as Config
 import Test.Hspec.JUnit.Render (renderJUnit)
-import qualified Test.Hspec.JUnit.Schema as Schema
+import Test.Hspec.JUnit.Schema qualified as Schema
 import Text.XML.Stream.Render (def, renderBytes)
 
 junit :: JUnitConfig -> FormatConfig -> IO Format
@@ -40,20 +38,20 @@
       groups = groupItems paths
       output =
         Schema.Suites
-          { suitesName = suiteName
-          , suitesSuites =
+          { name = suiteName
+          , suites =
               groups <&> \(group, items) -> do
                 let suite xs =
                       Schema.Suite
-                        { suiteName = group
-                        , suiteTimestamp = time
-                        , suiteCases = xs
+                        { name = group
+                        , timestamp = time
+                        , cases = xs
                         }
                 suite $ uncurry (itemToTestCase applyPrefix group) <$> items
           }
 
-    runConduitRes $
-      sourceList [output]
+    runConduitRes
+      $ yield output
         .| renderJUnit dropConsoleFormatting
         .| renderBytes def
         .| sinkFile file
@@ -73,40 +71,37 @@
   :: (FilePath -> FilePath) -> Text -> Text -> Item -> Schema.TestCase
 itemToTestCase applyPrefix group name item =
   Schema.TestCase
-    { testCaseLocation =
+    { location =
         toSchemaLocation applyPrefix
           <$> (itemResultLocation item <|> itemLocation item)
-    , testCaseClassName = group
-    , testCaseName = name
-    , testCaseDuration = unSeconds $ itemDuration item
-    , testCaseResult = case itemResult item of
+    , className = group
+    , name = name
+    , duration = unSeconds $ itemDuration item
+    , result = case itemResult item of
         Success -> Nothing
         Pending mLocation mMessage ->
-          Just $
-            Schema.Skipped $
-              prefixLocation mLocation $
-                prefixInfo $
-                  maybe
-                    ""
-                    pack
-                    mMessage
+          Just
+            $ Schema.Skipped
+            $ prefixLocation mLocation
+            $ prefixInfo
+            $ maybe "" pack mMessage
         Failure mLocation reason ->
-          Just $
-            Schema.Failure "error" $
-              prefixLocation mLocation $
-                prefixInfo $
-                  reasonToText reason
+          Just
+            $ Schema.Failure "error"
+            $ prefixLocation mLocation
+            $ prefixInfo
+            $ reasonToText reason
     }
  where
   prefixLocation mLocation str = case mLocation of
     Nothing -> str
-    Just Location {..} ->
+    Just l ->
       mconcat
-        [ pack $ applyPrefix locationFile
+        [ pack $ applyPrefix $ locationFile l
         , ":"
-        , pack $ show locationLine
+        , pack $ show $ locationLine l
         , ":"
-        , pack $ show locationColumn
+        , pack $ show $ locationColumn l
         , "\n"
         ]
         <> str
@@ -121,10 +116,10 @@
   Failure mLocation _ -> mLocation
 
 toSchemaLocation :: (FilePath -> FilePath) -> Location -> Schema.Location
-toSchemaLocation applyPrefix Location {..} =
+toSchemaLocation applyPrefix l =
   Schema.Location
-    { Schema.locationFile = applyPrefix locationFile
-    , Schema.locationLine = fromIntegral $ max 0 locationLine
+    { Schema.file = applyPrefix $ locationFile l
+    , Schema.line = fromIntegral $ max 0 $ locationLine l
     }
 
 unSeconds :: Seconds -> Double
@@ -136,8 +131,8 @@
   NoReason -> "no reason"
   Reason err -> pack err
   ExpectedButGot preface expected actual ->
-    T.unlines $
-      pack
+    T.unlines
+      $ pack
         <$> fromMaybe "" preface
           : ( foundLines "expected" expected
                 <> foundLines " but got" actual
diff --git a/library/Test/Hspec/JUnit/Formatter.hs b/library/Test/Hspec/JUnit/Formatter.hs
--- a/library/Test/Hspec/JUnit/Formatter.hs
+++ b/library/Test/Hspec/JUnit/Formatter.hs
@@ -8,7 +8,7 @@
 -- import Test.Hspec.JUnit.Config
 -- import Test.Hspec.JUnit.Formatter qualified as Formatter
 --
--- -- | To only produce a JUnit file, silencing other output
+-- -- | To produce only a JUnit file, silencing other output
 -- hook :: Spec -> Spec
 -- hook = Formatter.use defaultJUnitConfig
 --
@@ -16,7 +16,7 @@
 -- hook :: Spec -> Spec
 -- hook = Formatter.add defaultJUnitConfig
 --
--- -- | To only produce, but only when @--format=junit@ is used
+-- -- | To produce only a JUnit file, and only when @--format=junit@ is used
 -- hook :: Spec -> Spec
 -- hook = Formatter.register defaultJUnitConfig
 -- @
@@ -40,10 +40,9 @@
 import Prelude
 
 import Data.Maybe (fromMaybe)
-
 import Test.Hspec.Api.Format.V1 as Api
-import qualified Test.Hspec.Core.Format as Core
-import qualified Test.Hspec.Core.Formatters.V2 as V2
+import Test.Hspec.Core.Format qualified as Core
+import Test.Hspec.Core.Formatters.V2 qualified as V2
 import Test.Hspec.Core.Runner as Core (Config (..))
 import Test.Hspec.JUnit.Config
 import Test.Hspec.JUnit.Format
diff --git a/library/Test/Hspec/JUnit/Formatter/Env.hs b/library/Test/Hspec/JUnit/Formatter/Env.hs
--- a/library/Test/Hspec/JUnit/Formatter/Env.hs
+++ b/library/Test/Hspec/JUnit/Formatter/Env.hs
@@ -30,7 +30,7 @@
 import Test.Hspec.Core.Spec (runIO)
 import Test.Hspec.JUnit.Config
 import Test.Hspec.JUnit.Config.Env
-import qualified Test.Hspec.JUnit.Formatter as JUnit
+import Test.Hspec.JUnit.Formatter qualified as JUnit
 
 whenEnabled :: (SpecWith a -> SpecWith a) -> SpecWith a -> SpecWith a
 whenEnabled hook spec = do
diff --git a/library/Test/Hspec/JUnit/Render.hs b/library/Test/Hspec/JUnit/Render.hs
--- a/library/Test/Hspec/JUnit/Render.hs
+++ b/library/Test/Hspec/JUnit/Render.hs
@@ -4,13 +4,13 @@
 
 import Prelude
 
+import Conduit (ConduitT, awaitForever, mergeSource, yield, yieldMany, (.|))
 import Control.Monad.Catch (MonadThrow)
-import qualified Data.Array as Array
-import Data.Conduit (ConduitT, awaitForever, mergeSource, yield, (.|))
-import qualified Data.Conduit.List as CL
+import Data.Array qualified as Array
+import Data.Conduit.List qualified as CL
 import Data.Foldable (traverse_)
 import Data.Text (Text, pack)
-import qualified Data.Text as Text
+import Data.Text qualified as Text
 import Data.Time.ISO8601 (formatISO8601)
 import Data.XML.Types (Event)
 import Test.Hspec.JUnit.Schema
@@ -21,46 +21,37 @@
   , TestCase (..)
   )
 import Text.Printf
-import qualified Text.Regex.Base as Regex
-import qualified Text.Regex.TDFA.Text as Regex
+import Text.Regex.Base qualified as Regex
+import Text.Regex.TDFA.Text qualified as Regex
 import Text.XML.Stream.Render (attr, content, tag)
 
 renderJUnit :: MonadThrow m => Bool -> ConduitT Suites Event m ()
-renderJUnit shouldDropConsoleFormatting = awaitForever $ \Suites {..} ->
-  tag "testsuites" (attr "name" suitesName) $
-    CL.sourceList suitesSuites
+renderJUnit shouldDropConsoleFormatting = awaitForever $ \s ->
+  tag "testsuites" (attr "name" s.name)
+    $ yieldMany s.suites
       .| mergeSource idStream
       .| suite shouldDropConsoleFormatting
  where
   idStream = CL.iterate (+ 1) 0
 
 suite :: MonadThrow m => Bool -> ConduitT (Int, Suite) Event m ()
-suite shouldDropConsoleFormatting = awaitForever $ \(i, theSuite@Suite {..}) ->
-  tag "testsuite" (attributes i theSuite) $ do
+suite shouldDropConsoleFormatting = awaitForever $ \(i, s) ->
+  tag "testsuite" (attributes i s) $ do
     tag "properties" mempty mempty
-    CL.sourceList suiteCases .| do
-      awaitForever $ \x -> yield x .| testCase shouldDropConsoleFormatting
+    yieldMany s.cases .| testCase shouldDropConsoleFormatting
  where
   -- TODO these need to be made real values
-  attributes i Suite {..} =
-    attr "name" suiteName
-      <> attr "package" suiteName
+  attributes i s =
+    attr "name" s.name
+      <> attr "package" s.name
       <> attr "id" (tshow i)
-      <> attr "time" (roundToStr $ sumDurations suiteCases)
-      <> attr "timestamp" (pack $ formatISO8601 suiteTimestamp)
+      <> attr "time" (roundToStr $ sumDurations s.cases)
+      <> attr "timestamp" (pack $ formatISO8601 s.timestamp)
       <> attr "hostname" "localhost"
-      <> attr "tests" (tshow $ length suiteCases)
-      <> attr
-        "failures"
-        ( tshow $
-            length [() | Just Failure {} <- testCaseResult <$> suiteCases]
-        )
+      <> attr "tests" (tshow $ length s.cases)
+      <> attr "failures" (tshow $ length [() | Just Failure {} <- (.result) <$> s.cases])
       <> attr "errors" "0"
-      <> attr
-        "skipped"
-        ( tshow $
-            length [() | Just Skipped {} <- testCaseResult <$> suiteCases]
-        )
+      <> attr "skipped" (tshow $ length [() | Just Skipped {} <- (.result) <$> s.cases])
 
 tshow :: Show a => a -> Text
 tshow = pack . show
@@ -68,13 +59,13 @@
 testCase :: MonadThrow m => Bool -> ConduitT TestCase Event m ()
 testCase shouldDropConsoleFormatting =
   awaitForever $ \(TestCase mLocation className name duration mResult) ->
-    tag "testcase" (attributes mLocation className name duration) $
-      traverse_ yield mResult
+    tag "testcase" (attributes mLocation className name duration)
+      $ traverse_ yield mResult
         .| result shouldDropConsoleFormatting
  where
   attributes mLocation className name duration =
-    maybe mempty (attr "file" . pack . locationFile) mLocation
-      <> maybe mempty (attr "line" . pack . show . locationLine) mLocation
+    maybe mempty (attr "file" . pack . (.file)) mLocation
+      <> maybe mempty (attr "line" . pack . show . (.line)) mLocation
       <> attr "name" name
       <> attr "classname" className
       <> attr "time" (roundToStr duration)
@@ -106,7 +97,7 @@
     | otherwise = input
 
 sumDurations :: [TestCase] -> Double
-sumDurations cases = sum $ testCaseDuration <$> cases
+sumDurations cases = sum $ (.duration) <$> cases
 
 roundToStr :: PrintfArg a => a -> Text
 roundToStr = pack . printf "%0.9f"
diff --git a/library/Test/Hspec/JUnit/Schema.hs b/library/Test/Hspec/JUnit/Schema.hs
--- a/library/Test/Hspec/JUnit/Schema.hs
+++ b/library/Test/Hspec/JUnit/Schema.hs
@@ -13,30 +13,30 @@
 import Numeric.Natural
 
 data Suites = Suites
-  { suitesName :: Text
-  , suitesSuites :: [Suite]
+  { name :: Text
+  , suites :: [Suite]
   }
   deriving stock (Show)
 
 data Suite = Suite
-  { suiteName :: Text
-  , suiteTimestamp :: UTCTime
-  , suiteCases :: [TestCase]
+  { name :: Text
+  , timestamp :: UTCTime
+  , cases :: [TestCase]
   }
   deriving stock (Show)
 
 data TestCase = TestCase
-  { testCaseLocation :: Maybe Location
-  , testCaseClassName :: Text
-  , testCaseName :: Text
-  , testCaseDuration :: Double
-  , testCaseResult :: Maybe Result
+  { location :: Maybe Location
+  , className :: Text
+  , name :: Text
+  , duration :: Double
+  , result :: Maybe Result
   }
   deriving stock (Show)
 
 data Location = Location
-  { locationFile :: FilePath
-  , locationLine :: Natural
+  { file :: FilePath
+  , line :: Natural
   }
   deriving stock (Show)
 
diff --git a/tests/Example.hs b/tests/Example.hs
--- a/tests/Example.hs
+++ b/tests/Example.hs
@@ -5,9 +5,9 @@
 -- In fact, this sentence was added to retain them across a formatting change.
 module Example (spec) where
 
-import Control.Exception
 import Prelude
 
+import Control.Exception
 import Test.Hspec
 
 spec :: Spec
diff --git a/tests/Test/Hspec/JUnit/FormatterSpec.hs b/tests/Test/Hspec/JUnit/FormatterSpec.hs
--- a/tests/Test/Hspec/JUnit/FormatterSpec.hs
+++ b/tests/Test/Hspec/JUnit/FormatterSpec.hs
@@ -8,26 +8,26 @@
 
 import Control.Monad (void)
 import Data.Char (isSpace)
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import qualified Example
+import Data.List (isInfixOf, isPrefixOf)
+import Data.Map.Strict qualified as Map
+import Data.Text qualified as T
+import Example qualified
 import System.FilePath ((<.>), (</>))
 import System.IO.Temp (withSystemTempDirectory)
 import Test.Hspec
 import Test.Hspec.Golden
 import Test.Hspec.JUnit.Config
-import qualified Test.Hspec.JUnit.Formatter as Formatter
+import Test.Hspec.JUnit.Formatter qualified as Formatter
 import Test.Hspec.Runner
-import qualified Text.XML as XML
+import Text.XML qualified as XML
 
 spec :: Spec
 spec = do
-  it "matches golden file" $
+  it "matches golden file" $ do
     junitGolden "default" id
 
-  it "matches golden file with prefixing" $
-    junitGolden "prefixed" $
-      setJUnitConfigSourcePathPrefix "lol/monorepo"
+  it "matches golden file with prefixing" $ do
+    junitGolden "prefixed" $ setJUnitConfigSourcePathPrefix "lol/monorepo"
 
 -- | Run @Example.spec@ and compare XML to a golden file
 junitGolden
@@ -39,10 +39,10 @@
 junitGolden name modifyConfig = do
   actual <- withSystemTempDirectory "" $ \tmp -> do
     let junitConfig =
-          modifyConfig $
-            setJUnitConfigOutputDirectory tmp $
-              setJUnitConfigOutputName "test.xml" $
-                defaultJUnitConfig "hspec-junit-format"
+          modifyConfig
+            $ setJUnitConfigOutputDirectory tmp
+            $ setJUnitConfigOutputName "test.xml"
+            $ defaultJUnitConfig "hspec-junit-format"
 
     runSpec' $ Formatter.use junitConfig Example.spec
     readNormalizedXML $ tmp </> "test.xml"
@@ -68,7 +68,7 @@
 readNormalizedXML = fmap normalizeDoc . XML.readFile XML.def
 
 normalizeDoc :: XML.Document -> XML.Document
-normalizeDoc = removeWhitespaceNodes . removeTimeAttributes
+normalizeDoc = removeWhitespaceNodes . removeTimeAttributes . normalizeErrorMessages
 
 removeWhitespaceNodes :: XML.Document -> XML.Document
 removeWhitespaceNodes doc =
@@ -101,6 +101,72 @@
       { XML.elementAttributes = Map.delete name $ XML.elementAttributes el
       , XML.elementNodes = map (onNodeElement go) $ XML.elementNodes el
       }
+
+  onNodeElement f = \case
+    XML.NodeElement el -> XML.NodeElement $ f el
+    n -> n
+
+normalizeErrorMessages :: XML.Document -> XML.Document
+normalizeErrorMessages doc =
+  doc
+    { XML.documentRoot = go $ XML.documentRoot doc
+    }
+ where
+  go el =
+    el
+      { XML.elementNodes =
+          map (onNodeElement go . normalizeErrorContent . normalizeLineNumbers)
+            $ XML.elementNodes el
+      }
+
+  normalizeErrorContent :: XML.Node -> XML.Node
+  normalizeErrorContent = \case
+    XML.NodeElement el
+      | XML.elementName el == XML.Name "failure" Nothing Nothing ->
+          XML.NodeElement
+            $ el {XML.elementNodes = map stripLocationPrefix $ XML.elementNodes el}
+      | XML.elementName el == XML.Name "skipped" Nothing Nothing ->
+          XML.NodeElement
+            $ el {XML.elementNodes = map stripLocationPrefix $ XML.elementNodes el}
+      | otherwise -> XML.NodeElement el
+    n -> n
+
+  normalizeLineNumbers :: XML.Node -> XML.Node
+  normalizeLineNumbers = \case
+    XML.NodeElement el
+      | XML.elementName el == XML.Name "testcase" Nothing Nothing ->
+          let
+            attrs = XML.elementAttributes el
+            normalizedAttrs = Map.adjust normalizeLineAttr (XML.Name "line" Nothing Nothing) attrs
+          in
+            XML.NodeElement $ el {XML.elementAttributes = normalizedAttrs}
+      | otherwise -> XML.NodeElement el
+    n -> n
+
+  normalizeLineAttr :: T.Text -> T.Text
+  normalizeLineAttr lineText
+    | lineText == "29" = "28" -- Normalize line 29 to 28 for version compatibility
+    | otherwise = lineText
+
+  stripLocationPrefix :: XML.Node -> XML.Node
+  stripLocationPrefix = \case
+    XML.NodeContent content ->
+      let
+        contentText = T.unpack content
+        normalizedContent = case lines contentText of
+          (firstLine : rest)
+            | ( "tests/Example.hs:" `isPrefixOf` firstLine
+                  || "lol/monorepo/tests/Example.hs:" `isPrefixOf` firstLine
+              )
+                && "\n" `isInfixOf` contentText ->
+                unlines rest
+          _ -> contentText
+        trimmedContent = case reverse normalizedContent of
+          '\n' : rest -> reverse rest
+          _ -> normalizedContent
+      in
+        XML.NodeContent $ T.pack trimmedContent
+    n -> n
 
   onNodeElement f = \case
     XML.NodeElement el -> XML.NodeElement $ f el
