diff --git a/antiope-sns.cabal b/antiope-sns.cabal
--- a/antiope-sns.cabal
+++ b/antiope-sns.cabal
@@ -1,14 +1,9 @@
-cabal-version: 1.12
-
--- This file has been generated from package.yaml by hpack version 0.31.1.
---
--- see: https://github.com/sol/hpack
---
--- hash: 50973aabaaad2ab917399cc0b8a236e46ccbb43b8534ed215c3cbcdae8fa42f6
+cabal-version: 2.2
 
 name:           antiope-sns
-version:        6.4.0
-description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>
+version:        7.0.0
+synopsis:       Please see the README on Github at <https://github.com/arbor/antiope#readme>
+description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>.
 category:       Services
 homepage:       https://github.com/arbor/antiope#readme
 bug-reports:    https://github.com/arbor/antiope/issues
@@ -31,10 +26,7 @@
       Antiope.SNS
       Antiope.SNS.Messages
       Antiope.SNS.Types
-  other-modules:
-      Paths_antiope_sns
-  hs-source-dirs:
-      src
+  hs-source-dirs: src
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2
   build-depends:
@@ -43,6 +35,7 @@
     , amazonka-core
     , amazonka-sns
     , base >=4.7 && <5
+    , bytestring
     , generic-lens
     , lens
     , text
@@ -54,9 +47,8 @@
   type: exitcode-stdio-1.0
   main-is: Spec.hs
   other-modules:
-      Paths_antiope_sns
-  hs-source-dirs:
-      test
+      Antiope.SNS.MessagesSpec
+  hs-source-dirs: test
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -msse4.2 -threaded -rtsopts -with-rtsopts=-N
   build-depends:
@@ -66,7 +58,11 @@
     , amazonka-sns
     , antiope-sns
     , base >=4.7 && <5
+    , bytestring
     , generic-lens
+    , hedgehog >=0.5 && <0.7
+    , hspec >=2.4 && <2.7
+    , hw-hspec-hedgehog >=0.1 && <0.3
     , lens
     , text
     , time
diff --git a/src/Antiope/SNS/Messages.hs b/src/Antiope/SNS/Messages.hs
--- a/src/Antiope/SNS/Messages.hs
+++ b/src/Antiope/SNS/Messages.hs
@@ -9,8 +9,9 @@
 import Data.Time.Clock (UTCTime)
 import GHC.Generics    (Generic)
 
-import qualified Data.Aeson.Types   as Aeson
-import qualified Data.Text.Encoding as T
+import qualified Data.Aeson.Types     as Aeson
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Text.Encoding   as Text
 
 data SnsMessage a = SnsMessage
   { type'     :: !(Maybe Text)
@@ -30,6 +31,16 @@
     <*> obj .:? "Timestamp"
     <*> decodeEscaped obj "Message"
 
+instance ToJSON a => ToJSON (SnsMessage a) where
+  toJSON msg = object
+    [ "Type"      .= type' msg
+    , "MessageId" .= messageId msg
+    , "TopicArn"  .= topicArn msg
+    , "Subject"   .= subject msg
+    , "Timestamp" .= timestamp msg
+    , "Message"   .= (Text.decodeUtf8 . LBS.toStrict . encode . message) msg
+    ]
+
 decodeEscaped :: FromJSON b => Object -> Text -> Aeson.Parser b
 decodeEscaped o t =
-  (o .: t) >>= (either fail pure . eitherDecodeStrict . T.encodeUtf8)
+  (o .: t) >>= (either fail pure . eitherDecodeStrict . Text.encodeUtf8)
diff --git a/test/Antiope/SNS/MessagesSpec.hs b/test/Antiope/SNS/MessagesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Antiope/SNS/MessagesSpec.hs
@@ -0,0 +1,49 @@
+module Antiope.SNS.MessagesSpec
+where
+
+import Antiope.SNS.Messages
+import Data.Aeson
+import Data.Time.Calendar
+import Data.Time.Clock
+
+import HaskellWorks.Hspec.Hedgehog
+import Hedgehog
+import Hedgehog.Gen                as Gen
+import Hedgehog.Range              as Range
+import Test.Hspec
+
+{-# ANN module ("HLint: Ignore Redundant do" :: String) #-}
+
+spec :: Spec
+spec = describe "Antiope.SNS.MessagesSpec" $ do
+  it "Can encode and decode SnsMessage" $ require $ property $ do
+    body <- forAll $ simpleJson
+    msg  <- forAll $ snsMessageGen body
+    tripping msg encode decode
+
+simpleJson :: MonadGen m => m Value
+simpleJson = do
+  txtVal <- Gen.text (Range.linear 1 25) Gen.alphaNum
+  numVal <- Gen.double (Range.linearFrac (-32000.0) 32000.0)
+  pure $ object [ "text" .= txtVal, "num" .= numVal ]
+
+snsMessageGen :: MonadGen m => a -> m (SnsMessage a)
+snsMessageGen a = do
+  let txtGen = Gen.text (Range.linear 1 25) Gen.alphaNum
+  SnsMessage
+    <$> Gen.maybe txtGen
+    <*> Gen.maybe txtGen
+    <*> Gen.maybe txtGen
+    <*> Gen.maybe txtGen
+    <*> Gen.maybe genUTCTime
+    <*> Gen.constant a
+
+genUTCTime :: MonadGen m => m UTCTime
+genUTCTime = do
+  y <- toInteger <$> Gen.int (Range.constant 2000 2019)
+  m <- Gen.int (Range.constant 1 12)
+  d <- Gen.int (Range.constant 1 28)
+  let day = fromGregorian y m d
+  secs <- toInteger <$> Gen.int (Range.constant 0 86401)
+  let diff = secondsToDiffTime secs
+  pure $ UTCTime day diff
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,2 +1,1 @@
-main :: IO ()
-main = putStrLn "Test suite not yet implemented"
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
