diff --git a/graflog.cabal b/graflog.cabal
--- a/graflog.cabal
+++ b/graflog.cabal
@@ -1,5 +1,5 @@
 name:                graflog
-version:             2.0.0
+version:             3.0.0
 synopsis:            Monadic correlated log events
 description:         Please see README.md
 homepage:            https://github.com/m-arnold/graflog#readme
@@ -21,6 +21,7 @@
                      , text
                      , aeson
                      , bytestring
+                     , containers
   default-language:    Haskell2010
   default-extensions:  GeneralizedNewtypeDeriving DeriveFunctor FlexibleContexts FlexibleInstances
 
@@ -35,6 +36,7 @@
                      , mtl
                      , text
                      , aeson
+                     , containers
   ghc-options:         -threaded -rtsopts -with-rtsopts=-N
   default-language:    Haskell2010
   default-extensions:  ConstraintKinds DeriveGeneric FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns OverloadedStrings RankNTypes ScopedTypeVariables TypeOperators DuplicateRecordFields
diff --git a/src/Graflog/Logger.hs b/src/Graflog/Logger.hs
--- a/src/Graflog/Logger.hs
+++ b/src/Graflog/Logger.hs
@@ -7,9 +7,13 @@
   , CorrelationId(..)
   , EventId(..)
   , Action(..)
-  , Protected(..)
   , logEvent'
   , jsonEncode
+  , ToLog(..)
+  , Log(..)
+  , numToLog
+  , dictionary
+  , pair
   ) where
 
 import Data.Aeson
@@ -17,21 +21,66 @@
 import Data.ByteString (ByteString)
 import Data.ByteString.Lazy (toStrict)
 import Data.Maybe (fromJust)
-import Data.String (IsString)
+import Data.Map (Map(..))
+import qualified Data.Map as Map
+import Data.String (IsString(..))
 import Data.Text (Text(..))
-import Data.Text.Conversions (decodeConvertText, UTF8(..))
+import Data.Text.Conversions (decodeConvertText, UTF8(..), toText)
 
 import Graflog.Console
 
-newtype Protected a = Protected { unProtect :: a }
-  deriving (Functor, Show, Eq)
+data Log
+  = Dictionary (Map Text Log)
+  | List [Log]
+  | Message Text
+  | Redacted
+  deriving (Eq, Show)
 
-instance ToJSON a => ToJSON (Protected a) where
-  toJSON (Protected a) = "(REDACTED)"
+instance IsString Log where
+  fromString = Message . toText
 
-instance FromJSON a => FromJSON (Protected a) where
-  parseJSON v = Protected <$> parseJSON v
+class ToLog a where
+  toLog :: a -> Log
 
+instance ToLog Log where
+  toLog = id
+
+instance ToLog Text where
+  toLog = Message
+
+instance ToLog a => ToLog [a] where
+  toLog = List . fmap toLog
+
+numToLog :: (Show a, Num a) => a -> Log
+numToLog = Message . toText . show
+
+instance ToLog Int where
+  toLog = numToLog
+
+instance ToLog Integer where
+  toLog = numToLog
+
+instance ToLog Float where
+  toLog = numToLog
+
+instance ToLog Double where
+  toLog = numToLog
+
+instance ToLog a => ToLog (Map Text a) where
+  toLog = Dictionary . fmap toLog
+
+dictionary :: [(Text, Log)] -> Log
+dictionary = Dictionary . Map.fromList
+
+pair :: ToLog a => Text -> a -> (Text, Log)
+pair key value = (key, toLog value)
+
+instance ToJSON Log where
+  toJSON (Message a) = String a
+  toJSON (List a) = toJSON a
+  toJSON (Dictionary a) = toJSON a
+  toJSON Redacted = String "(REDACTED)"
+
 class Monad m => Logger m where
   logEvent :: Event -> m ()
 
@@ -50,7 +99,6 @@
   , _action :: Action
   , _message :: Value
   } deriving (Eq, Show)
-
 deriveJSON defaultOptions{fieldLabelModifier = drop 1} ''Event
 
 logEvent' :: Console m => Event -> m ()
