diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
 3.  Parse a replay with `stack exec octane path/to/the.replay`.
 
 [Octane]: https://github.com/tfausak/octane
-[Version badge]: https://www.stackage.org/package/octane/badge/nightly?label
+[Version badge]: https://www.stackage.org/package/octane/badge/nightly?label=version
 [package version]: https://www.stackage.org/package/octane
 [Build badge]: https://travis-ci.org/tfausak/octane.svg?branch=main
 [build status]: https://travis-ci.org/tfausak/octane
diff --git a/benchmark/Octane/Type/ActorBench.hs b/benchmark/Octane/Type/ActorBench.hs
--- a/benchmark/Octane/Type/ActorBench.hs
+++ b/benchmark/Octane/Type/ActorBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.ActorBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Actor"
-    [
+    [ bench "decode basic" (nf decodeActor "\1\0\0\0\0\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Actor (PCString "") (Int32LE 0)))
     ]
+
+decodeActor :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Actor)
+decodeActor = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/CacheItemBench.hs b/benchmark/Octane/Type/CacheItemBench.hs
--- a/benchmark/Octane/Type/CacheItemBench.hs
+++ b/benchmark/Octane/Type/CacheItemBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.CacheItemBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "CacheItem"
-    [
+    [ bench "decode basic" (nf decodeCacheItem "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (CacheItem (Int32LE 0) (Int32LE 0) (Int32LE 0) (List [])))
     ]
+
+decodeCacheItem :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheItem)
+decodeCacheItem = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/CachePropertyBench.hs b/benchmark/Octane/Type/CachePropertyBench.hs
--- a/benchmark/Octane/Type/CachePropertyBench.hs
+++ b/benchmark/Octane/Type/CachePropertyBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.CachePropertyBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "CacheProperty"
-    [
+    [ bench "decode basic" (nf decodeCacheProperty "\0\0\0\0\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (CacheProperty (Int32LE 0) (Int32LE 0)))
     ]
+
+decodeCacheProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheProperty)
+decodeCacheProperty = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/KeyFrameBench.hs b/benchmark/Octane/Type/KeyFrameBench.hs
--- a/benchmark/Octane/Type/KeyFrameBench.hs
+++ b/benchmark/Octane/Type/KeyFrameBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.KeyFrameBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "KeyFrame"
-    [
+    [ bench "decode basic" (nf decodeKeyFrame "\0\0\0\0\0\0\0\0\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (KeyFrame (Float32LE 0.0) (Int32LE 0) (Int32LE 0)))
     ]
+
+decodeKeyFrame :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, KeyFrame)
+decodeKeyFrame = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/MarkBench.hs b/benchmark/Octane/Type/MarkBench.hs
--- a/benchmark/Octane/Type/MarkBench.hs
+++ b/benchmark/Octane/Type/MarkBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.MarkBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Mark"
-    [
+    [ bench "decode basic" (nf decodeMark "\1\0\0\0\0\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Mark (PCString "") (Int32LE 0)))
     ]
+
+decodeMark :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Mark)
+decodeMark = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/MessageBench.hs b/benchmark/Octane/Type/MessageBench.hs
--- a/benchmark/Octane/Type/MessageBench.hs
+++ b/benchmark/Octane/Type/MessageBench.hs
@@ -1,8 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.MessageBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Message"
-    [
+    [ bench "decode basic" (nf decodeMessage "\0\0\0\0\1\0\0\0\0\1\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Message (Int32LE 0) (PCString "") (PCString "")))
     ]
+
+decodeMessage :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Message)
+decodeMessage = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/BooleanBench.hs b/benchmark/Octane/Type/Primitive/BooleanBench.hs
--- a/benchmark/Octane/Type/Primitive/BooleanBench.hs
+++ b/benchmark/Octane/Type/Primitive/BooleanBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Boolean"
-    [ bench "decode basic" (whnf decodeBoolean "\0")
-    , bench "encode basic" (whnf Binary.encode (NewBoolean False))
+    [ bench "decode basic" (nf decodeBoolean "\0")
+    , bench "encode basic" (nf Binary.encode (Boolean False))
     ]
 
 decodeBoolean :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Boolean)
diff --git a/benchmark/Octane/Type/Primitive/DictionaryBench.hs b/benchmark/Octane/Type/Primitive/DictionaryBench.hs
--- a/benchmark/Octane/Type/Primitive/DictionaryBench.hs
+++ b/benchmark/Octane/Type/Primitive/DictionaryBench.hs
@@ -11,8 +11,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Dictionary"
-    [ bench "decode basic" (whnf decodeBooleanDictionary "\5\0\0\0None\0")
-    , bench "encode basic" (whnf Binary.encode (NewDictionary Map.empty :: Dictionary Boolean))
+    [ bench "decode basic" (nf decodeBooleanDictionary "\5\0\0\0None\0")
+    , bench "encode basic" (nf Binary.encode (Dictionary Map.empty :: Dictionary Boolean))
     ]
 
 decodeDictionary :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Dictionary a)
diff --git a/benchmark/Octane/Type/Primitive/Float32LEBench.hs b/benchmark/Octane/Type/Primitive/Float32LEBench.hs
--- a/benchmark/Octane/Type/Primitive/Float32LEBench.hs
+++ b/benchmark/Octane/Type/Primitive/Float32LEBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Float32LE"
-    [ bench "decode basic" (whnf decodeFloat32LE "\0\0\0\0")
-    , bench "encode basic" (whnf Binary.encode (NewFloat32LE 0.0))
+    [ bench "decode basic" (nf decodeFloat32LE "\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Float32LE 0.0))
     ]
 
 decodeFloat32LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Float32LE)
diff --git a/benchmark/Octane/Type/Primitive/Int32LEBench.hs b/benchmark/Octane/Type/Primitive/Int32LEBench.hs
--- a/benchmark/Octane/Type/Primitive/Int32LEBench.hs
+++ b/benchmark/Octane/Type/Primitive/Int32LEBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Int32LE"
-    [ bench "decode basic" (whnf decodeInt32LE "\0\0\0\0")
-    , bench "encode basic" (whnf Binary.encode (NewInt32LE 0))
+    [ bench "decode basic" (nf decodeInt32LE "\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Int32LE 0))
     ]
 
 decodeInt32LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int32LE)
diff --git a/benchmark/Octane/Type/Primitive/Int64LEBench.hs b/benchmark/Octane/Type/Primitive/Int64LEBench.hs
--- a/benchmark/Octane/Type/Primitive/Int64LEBench.hs
+++ b/benchmark/Octane/Type/Primitive/Int64LEBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Int64LE"
-    [ bench "decode basic" (whnf decodeInt64LE "\0\0\0\0")
-    , bench "encode basic" (whnf Binary.encode (NewInt64LE 0))
+    [ bench "decode basic" (nf decodeInt64LE "\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (Int64LE 0))
     ]
 
 decodeInt64LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int64LE)
diff --git a/benchmark/Octane/Type/Primitive/ListBench.hs b/benchmark/Octane/Type/Primitive/ListBench.hs
--- a/benchmark/Octane/Type/Primitive/ListBench.hs
+++ b/benchmark/Octane/Type/Primitive/ListBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "List"
-    [ bench "decode basic" (whnf decodeBooleanList "\0\0\0\0")
-    , bench "encode basic" (whnf Binary.encode (NewList [] :: List Boolean))
+    [ bench "decode basic" (nf decodeBooleanList "\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (List [] :: List Boolean))
     ]
 
 decodeList :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, List a)
diff --git a/benchmark/Octane/Type/Primitive/PCStringBench.hs b/benchmark/Octane/Type/Primitive/PCStringBench.hs
--- a/benchmark/Octane/Type/Primitive/PCStringBench.hs
+++ b/benchmark/Octane/Type/Primitive/PCStringBench.hs
@@ -10,8 +10,8 @@
 
 benchmarks :: Benchmark
 benchmarks = bgroup "PCString"
-    [ bench "decode basic" (whnf decodePCString "\1\0\0\0\0")
-    , bench "encode basic" (whnf Binary.encode (NewPCString ""))
+    [ bench "decode basic" (nf decodePCString "\1\0\0\0\0")
+    , bench "encode basic" (nf Binary.encode (PCString ""))
     ]
 
 decodePCString :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, PCString)
diff --git a/benchmark/Octane/Type/PropertyBench.hs b/benchmark/Octane/Type/PropertyBench.hs
--- a/benchmark/Octane/Type/PropertyBench.hs
+++ b/benchmark/Octane/Type/PropertyBench.hs
@@ -1,8 +1,97 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.PropertyBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Property"
-    [
+    [ bgroup "Array"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\14\0\0\0ArrayProperty\0\
+            \\0\0\0\0\0\0\0\0\
+            \\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (ArrayProperty
+            (Int64LE 0)
+            (List [])))
+        ]
+    , bgroup "Bool"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\13\0\0\0BoolProperty\0\
+            \\0\0\0\0\0\0\0\0\
+            \\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (BoolProperty
+            (Int64LE 0)
+            (Boolean False)))
+        ]
+    , bgroup "Byte"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\13\0\0\0ByteProperty\0\
+            \\0\0\0\0\0\0\0\0\
+            \\1\0\0\0\0\
+            \\1\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (ByteProperty
+            (Int64LE 0)
+            (PCString "", PCString "")))
+        ]
+    , bgroup "Float"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\14\0\0\0FloatProperty\0\
+            \\4\0\0\0\0\0\0\0\
+            \\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (FloatProperty
+            (Int64LE 4)
+            (Float32LE 0.0)))
+        ]
+    , bgroup "Int"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\12\0\0\0IntProperty\0\
+            \\4\0\0\0\0\0\0\0\
+            \\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (IntProperty
+            (Int64LE 4)
+            (Int32LE 0)))
+        ]
+    , bgroup "Name"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\13\0\0\0NameProperty\0\
+            \\0\0\0\0\0\0\0\0\
+            \\1\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (NameProperty
+            (Int64LE 0)
+            (PCString "")))
+        ]
+    , bgroup "QWord"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\14\0\0\0QWordProperty\0\
+            \\8\0\0\0\0\0\0\0\
+            \\0\0\0\0\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (QWordProperty
+            (Int64LE 8)
+            (Int64LE 0)))
+        ]
+    , bgroup "String"
+        [ bench "decode basic" (nf decodeProperty "\
+            \\12\0\0\0StrProperty\0\
+            \\0\0\0\0\0\0\0\0\
+            \\1\0\0\0\0\
+            \")
+        , bench "encode basic" (nf Binary.encode (StrProperty
+            (Int64LE 0)
+            (PCString "")))
+        ]
     ]
+
+decodeProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Property)
+decodeProperty = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/ReplayBench.hs b/benchmark/Octane/Type/ReplayBench.hs
--- a/benchmark/Octane/Type/ReplayBench.hs
+++ b/benchmark/Octane/Type/ReplayBench.hs
@@ -1,8 +1,56 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.ReplayBench (benchmarks) where
 
 import Criterion
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.Map as Map
+import Octane
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Replay"
-    [
+    [ bench "decode basic" (nf decodeReplay "\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\1\0\0\0\0\
+        \\5\0\0\0None\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \\0\0\0\0\
+        \")
+    , bench "encode basic" (nf Binary.encode (Replay
+        (Int32LE 0)
+        (Int32LE 0)
+        (Int32LE 0)
+        (Int32LE 0)
+        (PCString "")
+        (Dictionary Map.empty)
+        (Int32LE 0)
+        (Int32LE 0)
+        (List [])
+        (List [])
+        ""
+        (List [])
+        (List [])
+        (List [])
+        (List [])
+        (List [])
+        (List [])
+        (List [])))
     ]
+
+decodeReplay :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Replay)
+decodeReplay = Binary.decodeOrFail
diff --git a/library/Octane/Core.hs b/library/Octane/Core.hs
--- a/library/Octane/Core.hs
+++ b/library/Octane/Core.hs
@@ -1,5 +1,6 @@
 module Octane.Core
-    ( module Control.Monad
+    ( module Control.DeepSeq
+    , module Control.Monad
     , module Data.Binary
     , module Data.Binary.Get
     , module Data.Binary.IEEE754
@@ -12,10 +13,12 @@
     , module Data.Map
     , module Data.Text
     , module Data.Text.Encoding
+    , module GHC.Generics
     , module System.Environment
     , module System.IO
     ) where
 
+import Control.DeepSeq (NFData)
 import Control.Monad (replicateM, when)
 import Data.Binary (Binary, Get, Put, decodeFileOrFail, encode, get, getWord8, put, putWord8)
 import Data.Binary.Get (ByteOffset, getByteString, getWord32le, getWord64le)
@@ -29,5 +32,6 @@
 import Data.Map (Map)
 import Data.Text (Text)
 import Data.Text.Encoding (decodeLatin1, decodeUtf16LE, encodeUtf16LE)
+import GHC.Generics (Generic)
 import System.Environment (getArgs)
 import System.IO (hPutStrLn, stderr, stdout)
diff --git a/library/Octane/Main.hs b/library/Octane/Main.hs
--- a/library/Octane/Main.hs
+++ b/library/Octane/Main.hs
@@ -2,7 +2,6 @@
 
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
 import Octane.Core
 import Octane.Type
 
@@ -18,119 +17,8 @@
     Left (offset, message) -> hPutStrLn stderr
         (file ++ " @ byte " ++ show offset ++ " - " ++ message)
     Right replay -> do
-        putStrLn file
-
         let inputSize = contents & BS.length & fromIntegral
         let outputSize = replay & encode & BSL.length
         when (inputSize /= outputSize) $ do
-            hPutStrLn stderr
-                ( "input size ("
-                ++ show inputSize
-                ++ ") not equal to output size ("
-                ++ show outputSize ++ ")!"
-                )
-        putStrLn ""
-
-        putStrLn "# SIZE 1 #\n"
-        print (getInt32LE (replaySize1 replay))
-        putStrLn ""
-
-        putStrLn "# CRC 1 #\n"
-        print (getInt32LE (replayCRC1 replay))
-        putStrLn ""
-
-        putStrLn "# VERSION #\n"
-        print (getInt32LE (replayVersion1 replay))
-        print (getInt32LE (replayVersion2 replay))
-        putStrLn ""
-
-        putStrLn "# LABEL #\n"
-        print (getPCString (replayLabel replay))
-        putStrLn ""
-
-        putStrLn "# PROPERTIES #\n"
-        mapM_
-            (\ (name, property) -> do
-                putStr (show (getPCString name) ++ "\t=> ")
-                debugProperty property)
-            (Map.assocs (getDictionary (replayProperties replay)))
-        putStrLn ""
-
-        putStrLn "# SIZE 2 #\n"
-        print (getInt32LE (replaySize2 replay))
-        putStrLn ""
-
-        putStrLn "# CRC 2 #\n"
-        print (getInt32LE (replayCRC2 replay))
-        putStrLn ""
-
-        putStrLn "# EFFECTS #\n"
-        mapM_
-            (\ effect -> do
-                print (getPCString effect))
-            (getList (replayEffects replay))
-        putStrLn ""
-
-        putStrLn "# KEY FRAMES #\n"
-        mapM_
-            (\ keyFrame -> do
-                putStrLn (show (getFloat32LE (keyFrameTime keyFrame)) ++ "s @ frame " ++ show (getInt32LE (keyFrameFrame keyFrame)) ++ " - bit " ++ show (getInt32LE (keyFramePosition keyFrame))))
-            (getList (replayKeyFrames replay))
-        putStrLn ""
-
-        putStrLn "# FRAMES #\n"
-        putStrLn (show (BS.length (replayFrames replay)) ++ " bytes")
-        putStrLn ""
-
-        putStrLn "# MESSAGES #\n"
-        mapM_
-            (\ message -> do
-                putStrLn (show (getPCString (messageName message)) ++ " @ frame " ++ show (getInt32LE (messageFrame message)) ++ ": " ++ show (getPCString (messageContent message))))
-            (getList (replayMessages replay))
-        putStrLn ""
-
-        putStrLn "# MARKS #\n"
-        mapM_
-            (\ goal -> do
-                putStrLn (show (getPCString (markLabel goal)) ++ " @ frame " ++ show (getInt32LE (markFrame goal))))
-            (getList (replayMarks replay))
-        putStrLn ""
-
-        putStrLn "# PACKAGES #\n"
-        mapM_
-            (\ package -> do
-                print (getPCString package))
-            (getList (replayPackages replay))
-        putStrLn ""
-
-        putStrLn "# OBJECTS #\n"
-
-        putStrLn "# NAMES #\n"
-        mapM_
-            (\ name -> do
-                print (getPCString name))
-            (getList (replayNames replay))
-        putStrLn ""
-
-        putStrLn "# ACTORS #\n"
-
-        putStrLn "# CACHE #\n"
-
-debugProperty :: Property -> IO ()
-debugProperty property = case property of
-    ArrayProperty _ (NewList array) -> do
-        putStrLn "[array]"
-        mapM_
-            (\ (NewDictionary dictionary) -> mapM_
-                (\ (NewPCString k, v) -> do
-                    putStr ("\t" ++ show k ++ "\t=> ")
-                    debugProperty v)
-                (Map.assocs dictionary) >> putStrLn "")
-            array
-    BoolProperty _ (NewBoolean value) -> print value
-    ByteProperty _ (NewPCString key, NewPCString value) -> putStrLn (show key ++ ": " ++ show value)
-    FloatProperty _ (NewFloat32LE value) -> print value
-    IntProperty _ (NewInt32LE value) -> print value
-    NameProperty _ (NewPCString value) -> putStrLn (show value ++ " [name]")
-    QWordProperty _ (NewInt64LE value) -> print value
-    StrProperty _ (NewPCString value) -> print value
+            hPutStrLn stderr ("input size (" ++ show inputSize ++ ") not equal to output size (" ++ show outputSize ++ ")!")
+        print replay
diff --git a/library/Octane/Type/Actor.hs b/library/Octane/Type/Actor.hs
--- a/library/Octane/Type/Actor.hs
+++ b/library/Octane/Type/Actor.hs
@@ -1,19 +1,22 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.Actor (Actor(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.PCString
 import Octane.Type.Primitive.Int32LE
 
-data Actor = NewActor
+data Actor = Actor
     { actorName :: PCString
     , actorTag :: Int32LE
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Actor where
     get = do
         name <- get
         tag <- get
-        return NewActor
+        return Actor
             { actorName = name
             , actorTag = tag
             }
diff --git a/library/Octane/Type/CacheItem.hs b/library/Octane/Type/CacheItem.hs
--- a/library/Octane/Type/CacheItem.hs
+++ b/library/Octane/Type/CacheItem.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.CacheItem (CacheItem(..)) where
 
 import Octane.Core
@@ -5,12 +8,12 @@
 import Octane.Type.Primitive.Int32LE
 import Octane.Type.Primitive.List
 
-data CacheItem = NewCacheItem
+data CacheItem = CacheItem
     { cacheItemTag :: Int32LE
     , cacheItemStart :: Int32LE
     , cacheItemEnd :: Int32LE
     , cacheItemCacheProperties :: List CacheProperty
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary CacheItem where
     get = do
@@ -18,7 +21,7 @@
         start <- get
         end <- get
         cacheProperties <- get
-        return NewCacheItem
+        return CacheItem
             { cacheItemTag = tag
             , cacheItemStart = start
             , cacheItemEnd = end
diff --git a/library/Octane/Type/CacheProperty.hs b/library/Octane/Type/CacheProperty.hs
--- a/library/Octane/Type/CacheProperty.hs
+++ b/library/Octane/Type/CacheProperty.hs
@@ -1,18 +1,21 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.CacheProperty (CacheProperty(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.Int32LE
 
-data CacheProperty = NewCacheProperty
+data CacheProperty = CacheProperty
     { cachePropertyIndex :: Int32LE
     , cachePropertyTag :: Int32LE
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary CacheProperty where
     get = do
         index <- get
         tag <- get
-        return NewCacheProperty
+        return CacheProperty
             { cachePropertyIndex = index
             , cachePropertyTag = tag
             }
diff --git a/library/Octane/Type/KeyFrame.hs b/library/Octane/Type/KeyFrame.hs
--- a/library/Octane/Type/KeyFrame.hs
+++ b/library/Octane/Type/KeyFrame.hs
@@ -1,21 +1,27 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.KeyFrame (KeyFrame(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.Float32LE
 import Octane.Type.Primitive.Int32LE
 
-data KeyFrame = NewKeyFrame
+-- | A key frame. Each key frame has the time since the beginning of the match,
+-- | the frame it corresponds to, and that frame's bit position in the network
+-- | stream.
+data KeyFrame = KeyFrame
     { keyFrameTime :: Float32LE
     , keyFrameFrame :: Int32LE
     , keyFramePosition :: Int32LE
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary KeyFrame where
     get = do
         time <- get
         frame <- get
         position <- get
-        return NewKeyFrame
+        return KeyFrame
             { keyFrameTime = time
             , keyFrameFrame = frame
             , keyFramePosition = position
diff --git a/library/Octane/Type/Mark.hs b/library/Octane/Type/Mark.hs
--- a/library/Octane/Type/Mark.hs
+++ b/library/Octane/Type/Mark.hs
@@ -1,19 +1,24 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.Mark (Mark(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.PCString
 import Octane.Type.Primitive.Int32LE
 
-data Mark = NewMark
+-- | A tick mark on the replay. The only thing that creates tick marks are
+-- | goals.
+data Mark = Mark
     { markLabel :: PCString
     , markFrame :: Int32LE
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Mark where
     get = do
         label <- get
         frame <- get
-        return NewMark
+        return Mark
             { markLabel = label
             , markFrame = frame
             }
diff --git a/library/Octane/Type/Message.hs b/library/Octane/Type/Message.hs
--- a/library/Octane/Type/Message.hs
+++ b/library/Octane/Type/Message.hs
@@ -1,21 +1,25 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.Message (Message(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.PCString
 import Octane.Type.Primitive.Int32LE
 
-data Message = NewMessage
+-- | A debugging message. Replays do not have any of these anymore.
+data Message = Message
     { messageFrame :: Int32LE
     , messageName :: PCString
     , messageContent :: PCString
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Message where
     get = do
         frame <- get
         name <- get
         content <- get
-        return NewMessage
+        return Message
             { messageFrame = frame
             , messageName = name
             , messageContent = content
diff --git a/library/Octane/Type/Primitive/Boolean.hs b/library/Octane/Type/Primitive/Boolean.hs
--- a/library/Octane/Type/Primitive/Boolean.hs
+++ b/library/Octane/Type/Primitive/Boolean.hs
@@ -1,17 +1,21 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.Boolean (Boolean(..)) where
 
 import Octane.Core
 
-newtype Boolean = NewBoolean
+-- | A boolean value, stored in the first bit of a byte.
+newtype Boolean = Boolean
     { getBoolean :: Bool
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Boolean where
     get = do
         boolean <- getWord8
         if boolean > 1
         then fail "out of bounds"
-        else boolean & fromIntegral & toEnum & NewBoolean & return
+        else boolean & fromIntegral & toEnum & Boolean & return
 
-    put (NewBoolean boolean) = do
+    put (Boolean boolean) = do
         boolean & fromEnum & fromIntegral & putWord8
diff --git a/library/Octane/Type/Primitive/Dictionary.hs b/library/Octane/Type/Primitive/Dictionary.hs
--- a/library/Octane/Type/Primitive/Dictionary.hs
+++ b/library/Octane/Type/Primitive/Dictionary.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Octane.Type.Primitive.Dictionary (Dictionary(..)) where
@@ -6,28 +8,30 @@
 import Octane.Core
 import Octane.Type.Primitive.PCString
 
-newtype Dictionary a = NewDictionary
+-- | A dictionary that maps strings to values. The dictionary is terminated by
+-- | the key "None".
+newtype Dictionary a = Dictionary
     { getDictionary :: Map PCString a
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance (Binary a) => Binary (Dictionary a) where
     get = do
         element <- getElement
         if Map.null element
         then do
-            element & NewDictionary & return
+            element & Dictionary & return
         else do
-            NewDictionary elements <- get
-            elements & Map.union element & NewDictionary & return
+            Dictionary elements <- get
+            elements & Map.union element & Dictionary & return
 
-    put (NewDictionary elements) = do
+    put (Dictionary elements) = do
         elements & Map.assocs & mapM_ putElement
-        "None" & NewPCString & put
+        "None" & PCString & put
 
 getElement :: (Binary a) => Get (Map PCString a)
 getElement = do
     key <- get
-    if key == NewPCString "None"
+    if key == PCString "None"
     then do
         return Map.empty
     else do
diff --git a/library/Octane/Type/Primitive/Float32LE.hs b/library/Octane/Type/Primitive/Float32LE.hs
--- a/library/Octane/Type/Primitive/Float32LE.hs
+++ b/library/Octane/Type/Primitive/Float32LE.hs
@@ -1,15 +1,19 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.Float32LE (Float32LE(..)) where
 
 import Octane.Core
 
-newtype Float32LE = NewFloat32LE
+-- | A 32-bit little-endian float.
+newtype Float32LE = Float32LE
     { getFloat32LE :: Float
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Float32LE where
     get = do
         float <- getFloat32le
-        float & NewFloat32LE & return
+        float & Float32LE & return
 
-    put (NewFloat32LE float) = do
+    put (Float32LE float) = do
         float & putFloat32le
diff --git a/library/Octane/Type/Primitive/Int32LE.hs b/library/Octane/Type/Primitive/Int32LE.hs
--- a/library/Octane/Type/Primitive/Int32LE.hs
+++ b/library/Octane/Type/Primitive/Int32LE.hs
@@ -1,15 +1,19 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.Int32LE (Int32LE(..)) where
 
 import Octane.Core
 
-newtype Int32LE = NewInt32LE
+-- | A 32-bit little-endian integer.
+newtype Int32LE = Int32LE
     { getInt32LE :: Int32
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Int32LE where
     get = do
         int <- getWord32le
-        int & fromIntegral & NewInt32LE & return
+        int & fromIntegral & Int32LE & return
 
-    put (NewInt32LE int) = do
+    put (Int32LE int) = do
         int & fromIntegral & putWord32le
diff --git a/library/Octane/Type/Primitive/Int64LE.hs b/library/Octane/Type/Primitive/Int64LE.hs
--- a/library/Octane/Type/Primitive/Int64LE.hs
+++ b/library/Octane/Type/Primitive/Int64LE.hs
@@ -1,15 +1,19 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.Int64LE (Int64LE(..)) where
 
 import Octane.Core
 
-newtype Int64LE = NewInt64LE
+-- | A 64-bit little-endian integer.
+newtype Int64LE = Int64LE
     { getInt64LE :: Int64
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Int64LE where
     get = do
         int <- getWord64le
-        int & fromIntegral & NewInt64LE & return
+        int & fromIntegral & Int64LE & return
 
-    put (NewInt64LE int) = do
+    put (Int64LE int) = do
         int & fromIntegral & putWord64le
diff --git a/library/Octane/Type/Primitive/List.hs b/library/Octane/Type/Primitive/List.hs
--- a/library/Octane/Type/Primitive/List.hs
+++ b/library/Octane/Type/Primitive/List.hs
@@ -1,18 +1,22 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.List (List(..)) where
 
 import Octane.Core
 import Octane.Type.Primitive.Int32LE
 
-newtype List a = NewList
+-- | A length-prefixed list.
+newtype List a = List
     { getList :: [a]
-    } deriving (Eq, Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance (Binary a) => Binary (List a) where
     get = do
-        (NewInt32LE size) <- get
+        (Int32LE size) <- get
         elements <- replicateM (fromIntegral size) get
-        elements & NewList & return
+        elements & List & return
 
-    put (NewList list) = do
-        list & length & fromIntegral & NewInt32LE & put
+    put (List list) = do
+        list & length & fromIntegral & Int32LE & put
         list & mapM_ put
diff --git a/library/Octane/Type/Primitive/PCString.hs b/library/Octane/Type/Primitive/PCString.hs
--- a/library/Octane/Type/Primitive/PCString.hs
+++ b/library/Octane/Type/Primitive/PCString.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 module Octane.Type.Primitive.PCString (PCString(..)) where
 
 import qualified Data.ByteString.Char8 as BS8
@@ -5,13 +8,14 @@
 import Octane.Core
 import Octane.Type.Primitive.Int32LE
 
-newtype PCString = NewPCString
+-- | A length-prefixed null-terminated string.
+newtype PCString = PCString
     { getPCString :: Text
-    } deriving (Eq, Ord, Show)
+    } deriving (Eq, Generic, NFData, Ord, Show)
 
 instance Binary PCString where
     get = do
-        (NewInt32LE size) <- get
+        (Int32LE size) <- get
         string <- if size == 0
             then fail "invalid size"
             else if size < 0
@@ -22,17 +26,17 @@
             else do
                 bytes <- getByteString (fromIntegral size)
                 bytes & decodeLatin1 & return
-        string & Text.dropEnd 1 & NewPCString & return
+        string & Text.dropEnd 1 & PCString & return
 
-    put (NewPCString string) = do
+    put (PCString string) = do
         let cString = Text.snoc string '\NUL'
         let size = cString & Text.length & fromIntegral
         if Text.all isLatin1 cString
         then do
-            size & NewInt32LE & put
+            size & Int32LE & put
             cString & encodeLatin1 & putByteString
         else do
-            size & negate & NewInt32LE & put
+            size & negate & Int32LE & put
             cString & encodeUtf16LE & putByteString
 
 encodeLatin1 :: Text -> ByteString
diff --git a/library/Octane/Type/Property.hs b/library/Octane/Type/Property.hs
--- a/library/Octane/Type/Property.hs
+++ b/library/Octane/Type/Property.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Octane.Type.Property (Property(..)) where
@@ -20,7 +22,7 @@
     | NameProperty Int64LE PCString
     | QWordProperty Int64LE Int64LE
     | StrProperty Int64LE PCString
-    deriving (Show)
+    deriving (Eq, Generic, NFData, Show)
 
 instance Binary Property where
     get = do
@@ -38,59 +40,59 @@
                 value <- get
                 ByteProperty size (key, value) & return
             "FloatProperty" -> do
-                value <- case size of
-                    NewInt64LE 4 -> get
-                    _ -> fail ("unknown FloatProperty size " ++ show size)
+                value <- case getInt64LE size of
+                    4 -> get
+                    x -> fail ("unknown FloatProperty size " ++ show x)
                 FloatProperty size value & return
             "IntProperty" -> do
-                value <- case size of
-                    NewInt64LE 4 -> get
-                    _ -> fail ("unknown IntProperty size " ++ show size)
+                value <- case getInt64LE size of
+                    4 -> get
+                    x -> fail ("unknown IntProperty size " ++ show x)
                 IntProperty size value & return
             "NameProperty" -> do
                 value <- get
                 NameProperty size value & return
+            "QWordProperty" -> do
+                value <- case getInt64LE size of
+                    8 -> get
+                    x -> fail ("unknown QWordProperty size " ++ show x)
+                QWordProperty size value & return
             "StrProperty" -> do
                 value <- get
                 StrProperty size value & return
-            "QWordProperty" -> do
-                value <- case size of
-                    NewInt64LE 8 -> get
-                    _ -> fail ("unknown QWordProperty size " ++ show size)
-                QWordProperty size value & return
-            _ -> fail ("unknown property type " ++ show kind)
+            x -> fail ("unknown property type " ++ show x)
 
     put property = case property of
         ArrayProperty size value -> do
-            "ArrayProperty" & NewPCString & put
+            "ArrayProperty" & PCString & put
             size & put
             value & put
         BoolProperty size value -> do
-            "BoolProperty" & NewPCString & put
+            "BoolProperty" & PCString & put
             size & put
             value & put
         ByteProperty size (key, value) -> do
-            "ByteProperty" & NewPCString & put
+            "ByteProperty" & PCString & put
             size & put
             key & put
             value & put
         FloatProperty size value -> do
-            "FloatProperty" & NewPCString & put
+            "FloatProperty" & PCString & put
             size & put
             value & put
         IntProperty size value -> do
-            "IntProperty" & NewPCString & put
+            "IntProperty" & PCString & put
             size & put
             value & put
         NameProperty size value -> do
-            "NameProperty" & NewPCString & put
+            "NameProperty" & PCString & put
             size & put
             value & put
         QWordProperty size value -> do
-            "QWordProperty" & NewPCString & put
+            "QWordProperty" & PCString & put
             size & put
             value & put
         StrProperty size value -> do
-            "StrProperty" & NewPCString & put
+            "StrProperty" & PCString & put
             size & put
             value & put
diff --git a/library/Octane/Type/Replay.hs b/library/Octane/Type/Replay.hs
--- a/library/Octane/Type/Replay.hs
+++ b/library/Octane/Type/Replay.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
 module Octane.Type.Replay (Replay(..)) where
 
 import qualified Data.ByteString as BS
@@ -13,7 +16,7 @@
 import Octane.Type.Property
 import Octane.Type.Primitive.Dictionary
 
-data Replay = NewReplay
+data Replay = Replay
     { replaySize1 :: Int32LE
     , replayCRC1 :: Int32LE
     , replayVersion1 :: Int32LE
@@ -32,7 +35,7 @@
     , replayNames :: List PCString
     , replayActors :: List Actor
     , replayCacheItems :: List CacheItem
-    } deriving (Show)
+    } deriving (Eq, Generic, NFData, Show)
 
 instance Binary Replay where
     get = do
@@ -54,7 +57,7 @@
         names <- get
         actors <- get
         cacheItems <- get
-        return NewReplay
+        return Replay
             { replaySize1 = size1
             , replayCRC1 = crc1
             , replayVersion1 = version1
@@ -97,11 +100,11 @@
 
 getFrameBytes :: Get ByteString
 getFrameBytes = do
-    NewInt32LE size <- get
+    Int32LE size <- get
     frames <- getByteString (fromIntegral size)
     return frames
 
 putFrameBytes :: ByteString -> Put
 putFrameBytes frames = do
-    frames & BS.length & fromIntegral & NewInt32LE & put
+    frames & BS.length & fromIntegral & Int32LE & put
     frames & putByteString
diff --git a/octane.cabal b/octane.cabal
--- a/octane.cabal
+++ b/octane.cabal
@@ -1,5 +1,5 @@
 name: octane
-version: 0.2.1
+version: 0.3.0
 cabal-version: >=1.10
 build-type: Simple
 license: MIT
@@ -46,6 +46,7 @@
         bytestring ==0.10.*,
         containers ==0.5.*,
         data-binary-ieee754 ==0.4.*,
+        deepseq ==1.4.*,
         text ==1.2.*
     default-language: Haskell2010
     hs-source-dirs: library
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
 install-ghc: true
 packages: [.]
 require-stack-version: '>=1.0'
-resolver: nightly-2016-01-31
+resolver: nightly-2016-02-01
diff --git a/test-suite/Octane/Type/ActorSpec.hs b/test-suite/Octane/Type/ActorSpec.hs
--- a/test-suite/Octane/Type/ActorSpec.hs
+++ b/test-suite/Octane/Type/ActorSpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.ActorSpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "Actor" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeActor "\1\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 9, Actor (PCString "") (Int32LE 0))
+        decodeActor "\2\0\0\0a\0\2\0\0\0" `shouldBe` Right ("", 10, Actor (PCString "a") (Int32LE 2))
+    it "can be encoded" $ do
+        Binary.encode (Actor (PCString "") (Int32LE 0)) `shouldBe` "\1\0\0\0\0\0\0\0\0"
+        Binary.encode (Actor (PCString "a") (Int32LE 2)) `shouldBe` "\2\0\0\0a\0\2\0\0\0"
+
+decodeActor :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Actor)
+decodeActor = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/CacheItemSpec.hs b/test-suite/Octane/Type/CacheItemSpec.hs
--- a/test-suite/Octane/Type/CacheItemSpec.hs
+++ b/test-suite/Octane/Type/CacheItemSpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.CacheItemSpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "CacheItem" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeCacheItem "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 16, CacheItem (Int32LE 0) (Int32LE 0) (Int32LE 0) (List []))
+        decodeCacheItem "\1\0\0\0\2\0\0\0\3\0\0\0\1\0\0\0\4\0\0\0\5\0\0\0" `shouldBe` Right ("", 24, CacheItem (Int32LE 1) (Int32LE 2) (Int32LE 3) (List [CacheProperty (Int32LE 4) (Int32LE 5)]))
+    it "can be encoded" $ do
+        Binary.encode (CacheItem (Int32LE 0) (Int32LE 0) (Int32LE 0) (List [])) `shouldBe` "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (CacheItem (Int32LE 1) (Int32LE 2) (Int32LE 3) (List [CacheProperty (Int32LE 4) (Int32LE 5)])) `shouldBe` "\1\0\0\0\2\0\0\0\3\0\0\0\1\0\0\0\4\0\0\0\5\0\0\0"
+
+decodeCacheItem :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheItem)
+decodeCacheItem = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/CachePropertySpec.hs b/test-suite/Octane/Type/CachePropertySpec.hs
--- a/test-suite/Octane/Type/CachePropertySpec.hs
+++ b/test-suite/Octane/Type/CachePropertySpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.CachePropertySpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "CacheProperty" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeCacheProperty "\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, CacheProperty (Int32LE 0) (Int32LE 0))
+        decodeCacheProperty "\1\0\0\0\2\0\0\0" `shouldBe` Right ("", 8, CacheProperty (Int32LE 1) (Int32LE 2))
+    it "can be encoded" $ do
+        Binary.encode (CacheProperty (Int32LE 0) (Int32LE 0)) `shouldBe` "\0\0\0\0\0\0\0\0"
+        Binary.encode (CacheProperty (Int32LE 1) (Int32LE 2)) `shouldBe` "\1\0\0\0\2\0\0\0"
+
+decodeCacheProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheProperty)
+decodeCacheProperty = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/KeyFrameSpec.hs b/test-suite/Octane/Type/KeyFrameSpec.hs
--- a/test-suite/Octane/Type/KeyFrameSpec.hs
+++ b/test-suite/Octane/Type/KeyFrameSpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.KeyFrameSpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "KeyFrame" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeKeyFrame "\0\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 12, KeyFrame (Float32LE 0.0) (Int32LE 0) (Int32LE 0))
+        decodeKeyFrame "\0\0\128\63\2\0\0\0\3\0\0\0" `shouldBe` Right ("", 12, KeyFrame (Float32LE 1.0) (Int32LE 2) (Int32LE 3))
+    it "can be encoded" $ do
+        Binary.encode (KeyFrame (Float32LE 0.0) (Int32LE 0) (Int32LE 0)) `shouldBe` "\0\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (KeyFrame (Float32LE 1.0) (Int32LE 2) (Int32LE 3)) `shouldBe` "\0\0\128\63\2\0\0\0\3\0\0\0"
+
+decodeKeyFrame :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, KeyFrame)
+decodeKeyFrame = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/MarkSpec.hs b/test-suite/Octane/Type/MarkSpec.hs
--- a/test-suite/Octane/Type/MarkSpec.hs
+++ b/test-suite/Octane/Type/MarkSpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.MarkSpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "Mark" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeMark "\1\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 9, Mark (PCString "") (Int32LE 0))
+        decodeMark "\2\0\0\0a\0\1\0\0\0" `shouldBe` Right ("", 10, Mark (PCString "a") (Int32LE 1))
+    it "can be encoded" $ do
+        Binary.encode (Mark (PCString "") (Int32LE 0)) `shouldBe` "\1\0\0\0\0\0\0\0\0"
+        Binary.encode (Mark (PCString "a") (Int32LE 1)) `shouldBe` "\2\0\0\0a\0\1\0\0\0"
+
+decodeMark :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Mark)
+decodeMark = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/MessageSpec.hs b/test-suite/Octane/Type/MessageSpec.hs
--- a/test-suite/Octane/Type/MessageSpec.hs
+++ b/test-suite/Octane/Type/MessageSpec.hs
@@ -1,7 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.MessageSpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "Message" $ do
-    return ()
+    it "can be decoded" $ do
+        decodeMessage "\0\0\0\0\1\0\0\0\0\1\0\0\0\0" `shouldBe` Right ("", 14, Message (Int32LE 0) (PCString "") (PCString ""))
+        decodeMessage "\1\0\0\0\2\0\0\0a\0\2\0\0\0b\0" `shouldBe` Right ("", 16, Message (Int32LE 1) (PCString "a") (PCString "b"))
+    it "can be encoded" $ do
+        Binary.encode (Message (Int32LE 0) (PCString "") (PCString "")) `shouldBe` "\0\0\0\0\1\0\0\0\0\1\0\0\0\0"
+        Binary.encode (Message (Int32LE 1) (PCString "a") (PCString "b")) `shouldBe` "\1\0\0\0\2\0\0\0a\0\2\0\0\0b\0"
+
+decodeMessage :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Message)
+decodeMessage = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/BooleanSpec.hs b/test-suite/Octane/Type/Primitive/BooleanSpec.hs
--- a/test-suite/Octane/Type/Primitive/BooleanSpec.hs
+++ b/test-suite/Octane/Type/Primitive/BooleanSpec.hs
@@ -11,11 +11,11 @@
 spec :: Spec
 spec = describe "Boolean" $ do
     it "can be decoded" $ do
-        decodeBoolean "\0" `shouldBe` Right ("", 1, NewBoolean False)
-        decodeBoolean "\1" `shouldBe` Right ("", 1, NewBoolean True)
+        decodeBoolean "\0" `shouldBe` Right ("", 1, Boolean False)
+        decodeBoolean "\1" `shouldBe` Right ("", 1, Boolean True)
     it "can be encoded" $ do
-        Binary.encode (NewBoolean False) `shouldBe` "\0"
-        Binary.encode (NewBoolean True) `shouldBe` "\1"
+        Binary.encode (Boolean False) `shouldBe` "\0"
+        Binary.encode (Boolean True) `shouldBe` "\1"
     it "does not raise a runtime error when decoding garbage" $ do
         decodeBoolean "garbage" `shouldBe` Left ("arbage", 1, "out of bounds")
 
diff --git a/test-suite/Octane/Type/Primitive/DictionarySpec.hs b/test-suite/Octane/Type/Primitive/DictionarySpec.hs
--- a/test-suite/Octane/Type/Primitive/DictionarySpec.hs
+++ b/test-suite/Octane/Type/Primitive/DictionarySpec.hs
@@ -12,13 +12,13 @@
 spec :: Spec
 spec = describe "Dictionary" $ do
     it "can be decoded" $ do
-        decodeDictionary "\5\0\0\0None\0" `shouldBe` Right ("", 9, NewDictionary Map.empty :: Dictionary Boolean)
-        decodeDictionary "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0" `shouldBe` Right ("", 29, NewDictionary (Map.singleton (NewPCString "Hello") (NewPCString "World")))
-        decodeDictionary "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0" `shouldBe` Right ("", 33, NewDictionary (Map.fromList [(NewPCString "truthy", NewBoolean True), (NewPCString "falsey", NewBoolean False)]))
+        decodeDictionary "\5\0\0\0None\0" `shouldBe` Right ("", 9, Dictionary Map.empty :: Dictionary Boolean)
+        decodeDictionary "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0" `shouldBe` Right ("", 29, Dictionary (Map.singleton (PCString "Hello") (PCString "World")))
+        decodeDictionary "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0" `shouldBe` Right ("", 33, Dictionary (Map.fromList [(PCString "truthy", Boolean True), (PCString "falsey", Boolean False)]))
     it "can be encoded" $ do
-        Binary.encode (NewDictionary Map.empty :: Dictionary Boolean) `shouldBe` "\5\0\0\0None\0"
-        Binary.encode (NewDictionary (Map.singleton (NewPCString "Hello") (NewPCString "World"))) `shouldBe` "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0"
-        Binary.encode (NewDictionary (Map.fromList [(NewPCString "truthy", NewBoolean True), (NewPCString "falsey", NewBoolean False)])) `shouldBe` "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0"
+        Binary.encode (Dictionary Map.empty :: Dictionary Boolean) `shouldBe` "\5\0\0\0None\0"
+        Binary.encode (Dictionary (Map.singleton (PCString "Hello") (PCString "World"))) `shouldBe` "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0"
+        Binary.encode (Dictionary (Map.fromList [(PCString "truthy", Boolean True), (PCString "falsey", Boolean False)])) `shouldBe` "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0"
 
 decodeDictionary :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Dictionary a)
 decodeDictionary = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Float32LESpec.hs b/test-suite/Octane/Type/Primitive/Float32LESpec.hs
--- a/test-suite/Octane/Type/Primitive/Float32LESpec.hs
+++ b/test-suite/Octane/Type/Primitive/Float32LESpec.hs
@@ -11,11 +11,11 @@
 spec :: Spec
 spec = describe "Float32LE" $ do
     it "can be decoded" $ do
-        decodeFloat32LE "\0\0\0\0" `shouldBe` Right ("", 4, NewFloat32LE 0.0)
-        decodeFloat32LE "\0\0\128\63" `shouldBe` Right ("", 4, NewFloat32LE 1.0)
+        decodeFloat32LE "\0\0\0\0" `shouldBe` Right ("", 4, Float32LE 0.0)
+        decodeFloat32LE "\0\0\128\63" `shouldBe` Right ("", 4, Float32LE 1.0)
     it "can be encoded" $ do
-        Binary.encode (NewFloat32LE 0.0) `shouldBe` "\0\0\0\0"
-        Binary.encode (NewFloat32LE 1.0) `shouldBe` "\0\0\128\63"
+        Binary.encode (Float32LE 0.0) `shouldBe` "\0\0\0\0"
+        Binary.encode (Float32LE 1.0) `shouldBe` "\0\0\128\63"
 
 decodeFloat32LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Float32LE)
 decodeFloat32LE = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Int32LESpec.hs b/test-suite/Octane/Type/Primitive/Int32LESpec.hs
--- a/test-suite/Octane/Type/Primitive/Int32LESpec.hs
+++ b/test-suite/Octane/Type/Primitive/Int32LESpec.hs
@@ -11,11 +11,11 @@
 spec :: Spec
 spec = describe "Int32LE" $ do
     it "can be decoded" $ do
-        decodeInt32LE "\0\0\0\0" `shouldBe` Right ("", 4, NewInt32LE 0)
-        decodeInt32LE "\1\0\0\0" `shouldBe` Right ("", 4, NewInt32LE 1)
+        decodeInt32LE "\0\0\0\0" `shouldBe` Right ("", 4, Int32LE 0)
+        decodeInt32LE "\1\0\0\0" `shouldBe` Right ("", 4, Int32LE 1)
     it "can be encoded" $ do
-        Binary.encode (NewInt32LE 0) `shouldBe` "\0\0\0\0"
-        Binary.encode (NewInt32LE 1) `shouldBe` "\1\0\0\0"
+        Binary.encode (Int32LE 0) `shouldBe` "\0\0\0\0"
+        Binary.encode (Int32LE 1) `shouldBe` "\1\0\0\0"
 
 decodeInt32LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int32LE)
 decodeInt32LE = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Int64LESpec.hs b/test-suite/Octane/Type/Primitive/Int64LESpec.hs
--- a/test-suite/Octane/Type/Primitive/Int64LESpec.hs
+++ b/test-suite/Octane/Type/Primitive/Int64LESpec.hs
@@ -11,11 +11,11 @@
 spec :: Spec
 spec = describe "Int64LE" $ do
     it "can be decoded" $ do
-        decodeInt64LE "\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, NewInt64LE 0)
-        decodeInt64LE "\1\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, NewInt64LE 1)
+        decodeInt64LE "\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, Int64LE 0)
+        decodeInt64LE "\1\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, Int64LE 1)
     it "can be encoded" $ do
-        Binary.encode (NewInt64LE 0) `shouldBe` "\0\0\0\0\0\0\0\0"
-        Binary.encode (NewInt64LE 1) `shouldBe` "\1\0\0\0\0\0\0\0"
+        Binary.encode (Int64LE 0) `shouldBe` "\0\0\0\0\0\0\0\0"
+        Binary.encode (Int64LE 1) `shouldBe` "\1\0\0\0\0\0\0\0"
 
 decodeInt64LE :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int64LE)
 decodeInt64LE = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/ListSpec.hs b/test-suite/Octane/Type/Primitive/ListSpec.hs
--- a/test-suite/Octane/Type/Primitive/ListSpec.hs
+++ b/test-suite/Octane/Type/Primitive/ListSpec.hs
@@ -11,13 +11,13 @@
 spec :: Spec
 spec = describe "List" $ do
     it "can be decoded" $ do
-        decodeList "\0\0\0\0" `shouldBe` Right ("", 4, NewList [] :: List Boolean)
-        decodeList "\1\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, NewList [NewInt32LE 0])
-        decodeList "\2\0\0\0\0\1" `shouldBe` Right ("", 6, NewList [NewBoolean False, NewBoolean True])
+        decodeList "\0\0\0\0" `shouldBe` Right ("", 4, List [] :: List Boolean)
+        decodeList "\1\0\0\0\0\0\0\0" `shouldBe` Right ("", 8, List [Int32LE 0])
+        decodeList "\2\0\0\0\0\1" `shouldBe` Right ("", 6, List [Boolean False, Boolean True])
     it "can be encoded" $ do
-        Binary.encode (NewList [] :: List Boolean) `shouldBe` "\0\0\0\0"
-        Binary.encode (NewList [NewInt32LE 0]) `shouldBe` "\1\0\0\0\0\0\0\0"
-        Binary.encode (NewList [NewBoolean False, NewBoolean True]) `shouldBe` "\2\0\0\0\0\1"
+        Binary.encode (List [] :: List Boolean) `shouldBe` "\0\0\0\0"
+        Binary.encode (List [Int32LE 0]) `shouldBe` "\1\0\0\0\0\0\0\0"
+        Binary.encode (List [Boolean False, Boolean True]) `shouldBe` "\2\0\0\0\0\1"
 
 decodeList :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, List a)
 decodeList = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/PCStringSpec.hs b/test-suite/Octane/Type/Primitive/PCStringSpec.hs
--- a/test-suite/Octane/Type/Primitive/PCStringSpec.hs
+++ b/test-suite/Octane/Type/Primitive/PCStringSpec.hs
@@ -11,13 +11,13 @@
 spec :: Spec
 spec = describe "PCString" $ do
     it "can be decoded" $ do
-        decodePCString "\1\0\0\0\0" `shouldBe` Right ("", 5, NewPCString "")
-        decodePCString "\6\0\0\0ascii\0" `shouldBe` Right ("", 10, NewPCString "ascii")
-        decodePCString "\8\0\0\0\251\241\239\231\248d\233\0" `shouldBe` Right ("", 12, NewPCString "ûñïçødé")
+        decodePCString "\1\0\0\0\0" `shouldBe` Right ("", 5, PCString "")
+        decodePCString "\6\0\0\0ascii\0" `shouldBe` Right ("", 10, PCString "ascii")
+        decodePCString "\8\0\0\0\251\241\239\231\248d\233\0" `shouldBe` Right ("", 12, PCString "ûñïçødé")
     it "can be encoded" $ do
-        Binary.encode (NewPCString "") `shouldBe` "\1\0\0\0\0"
-        Binary.encode (NewPCString "ascii") `shouldBe` "\6\0\0\0ascii\0"
-        Binary.encode (NewPCString "ûñïçødé") `shouldBe` "\8\0\0\0\251\241\239\231\248d\233\0"
+        Binary.encode (PCString "") `shouldBe` "\1\0\0\0\0"
+        Binary.encode (PCString "ascii") `shouldBe` "\6\0\0\0ascii\0"
+        Binary.encode (PCString "ûñïçødé") `shouldBe` "\8\0\0\0\251\241\239\231\248d\233\0"
     it "does not decode strings of length 0" $ do
         decodePCString "\0\0\0\0" `shouldBe` Left ("", 4, "invalid size")
 
diff --git a/test-suite/Octane/Type/PropertySpec.hs b/test-suite/Octane/Type/PropertySpec.hs
--- a/test-suite/Octane/Type/PropertySpec.hs
+++ b/test-suite/Octane/Type/PropertySpec.hs
@@ -1,7 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.PropertySpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.Map as Map
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "Property" $ do
-    return ()
+    it "can decode arrays" $ do
+        decodeProperty "\14\0\0\0ArrayProperty\0\0\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 30, ArrayProperty (Int64LE 0) (List []))
+        decodeProperty "\14\0\0\0ArrayProperty\0\1\0\0\0\0\0\0\0\1\0\0\0\1\0\0\0\0\13\0\0\0BoolProperty\0\0\0\0\0\0\0\0\0\0\5\0\0\0None\0" `shouldBe` Right ("", 70, ArrayProperty (Int64LE 1) (List [Dictionary (Map.singleton (PCString "") (BoolProperty (Int64LE 0) (Boolean False)))]))
+    it "can decode booleans" $ do
+        decodeProperty "\13\0\0\0BoolProperty\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 26, BoolProperty (Int64LE 0) (Boolean False))
+        decodeProperty "\13\0\0\0BoolProperty\0\1\0\0\0\0\0\0\0\1" `shouldBe` Right ("", 26, BoolProperty (Int64LE 1) (Boolean True))
+    it "can decode bytes" $ do
+        decodeProperty "\13\0\0\0ByteProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0\1\0\0\0\0" `shouldBe` Right ("", 35, ByteProperty (Int64LE 0) (PCString "", PCString ""))
+        decodeProperty "\13\0\0\0ByteProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0\2\0\0\0b\0" `shouldBe` Right ("", 37, ByteProperty (Int64LE 1) (PCString "a", PCString "b"))
+    it "can decode floats" $ do
+        decodeProperty "\14\0\0\0FloatProperty\0\4\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 30, FloatProperty (Int64LE 4) (Float32LE 0.0))
+        decodeProperty "\14\0\0\0FloatProperty\0\4\0\0\0\0\0\0\0\0\0\128\63" `shouldBe` Right ("", 30, FloatProperty (Int64LE 4) (Float32LE 1.0))
+    it "can decode integers" $ do
+        decodeProperty "\12\0\0\0IntProperty\0\4\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 28, IntProperty (Int64LE 4) (Int32LE 0))
+        decodeProperty "\12\0\0\0IntProperty\0\4\0\0\0\0\0\0\0\1\0\0\0" `shouldBe` Right ("", 28, IntProperty (Int64LE 4) (Int32LE 1))
+    it "can decode names" $ do
+        decodeProperty "\13\0\0\0NameProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0" `shouldBe` Right ("", 30, NameProperty (Int64LE 0) (PCString ""))
+        decodeProperty "\13\0\0\0NameProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0" `shouldBe` Right ("", 31, NameProperty (Int64LE 1) (PCString "a"))
+    it "can decode qwords" $ do
+        decodeProperty "\14\0\0\0QWordProperty\0\8\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" `shouldBe` Right ("", 34, QWordProperty (Int64LE 8) (Int64LE 0))
+        decodeProperty "\14\0\0\0QWordProperty\0\8\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0" `shouldBe` Right ("", 34, QWordProperty (Int64LE 8) (Int64LE 2))
+    it "can decode strings" $ do
+        decodeProperty "\12\0\0\0StrProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0" `shouldBe` Right ("", 29, StrProperty (Int64LE 0) (PCString ""))
+        decodeProperty "\12\0\0\0StrProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0" `shouldBe` Right ("", 30, StrProperty (Int64LE 1) (PCString "a"))
+    it "can encode arrays" $ do
+        Binary.encode (ArrayProperty (Int64LE 0) (List [])) `shouldBe` "\14\0\0\0ArrayProperty\0\0\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (ArrayProperty (Int64LE 1) (List [Dictionary (Map.singleton (PCString "a") (BoolProperty (Int64LE 2) (Boolean True)))])) `shouldBe` "\14\0\0\0ArrayProperty\0\1\0\0\0\0\0\0\0\1\0\0\0\2\0\0\0a\0\13\0\0\0BoolProperty\0\2\0\0\0\0\0\0\0\1\5\0\0\0None\0"
+    it "can encode booleans" $ do
+        Binary.encode (BoolProperty (Int64LE 0) (Boolean False)) `shouldBe` "\13\0\0\0BoolProperty\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (BoolProperty (Int64LE 1) (Boolean True)) `shouldBe` "\13\0\0\0BoolProperty\0\1\0\0\0\0\0\0\0\1"
+    it "can encode bytes" $ do
+        Binary.encode (ByteProperty (Int64LE 0) (PCString "", PCString "")) `shouldBe` "\13\0\0\0ByteProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0\1\0\0\0\0"
+        Binary.encode (ByteProperty (Int64LE 1) (PCString "a", PCString "b")) `shouldBe` "\13\0\0\0ByteProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0\2\0\0\0b\0"
+    it "can encode floats" $ do
+        Binary.encode (FloatProperty (Int64LE 4) (Float32LE 0.0)) `shouldBe` "\14\0\0\0FloatProperty\0\4\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (FloatProperty (Int64LE 4) (Float32LE 1.0)) `shouldBe` "\14\0\0\0FloatProperty\0\4\0\0\0\0\0\0\0\0\0\128\63"
+    it "can encode integers" $ do
+        Binary.encode (IntProperty (Int64LE 4) (Int32LE 0)) `shouldBe` "\12\0\0\0IntProperty\0\4\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (IntProperty (Int64LE 4) (Int32LE 1)) `shouldBe` "\12\0\0\0IntProperty\0\4\0\0\0\0\0\0\0\1\0\0\0"
+    it "can encode names" $ do
+        Binary.encode (NameProperty (Int64LE 0) (PCString "")) `shouldBe` "\13\0\0\0NameProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0"
+        Binary.encode (NameProperty (Int64LE 1) (PCString "a")) `shouldBe` "\13\0\0\0NameProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0"
+    it "can encode qwords" $ do
+        Binary.encode (QWordProperty (Int64LE 8) (Int64LE 0)) `shouldBe` "\14\0\0\0QWordProperty\0\8\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+        Binary.encode (QWordProperty (Int64LE 8) (Int64LE 2)) `shouldBe` "\14\0\0\0QWordProperty\0\8\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0"
+    it "can encode strings" $ do
+        Binary.encode (StrProperty (Int64LE 0) (PCString "")) `shouldBe` "\12\0\0\0StrProperty\0\0\0\0\0\0\0\0\0\1\0\0\0\0"
+        Binary.encode (StrProperty (Int64LE 1) (PCString "a")) `shouldBe` "\12\0\0\0StrProperty\0\1\0\0\0\0\0\0\0\2\0\0\0a\0"
+    it "does not raise a runtime error when decoding garbage" $ do
+        decodeProperty "\14\0\0\0OtherProperty\0\0\0\0\0\0\0\0\0" `shouldBe` Left ("", 26, "unknown property type \"OtherProperty\"")
+        decodeProperty "\14\0\0\0FloatProperty\0\0\0\0\0\0\0\0\0" `shouldBe` Left ("", 26, "unknown FloatProperty size 0")
+        decodeProperty "\12\0\0\0IntProperty\0\0\0\0\0\0\0\0\0" `shouldBe` Left ("", 24, "unknown IntProperty size 0")
+        decodeProperty "\14\0\0\0QWordProperty\0\0\0\0\0\0\0\0\0" `shouldBe` Left ("", 26, "unknown QWordProperty size 0")
+
+decodeProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Property)
+decodeProperty = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/ReplaySpec.hs b/test-suite/Octane/Type/ReplaySpec.hs
--- a/test-suite/Octane/Type/ReplaySpec.hs
+++ b/test-suite/Octane/Type/ReplaySpec.hs
@@ -1,7 +1,98 @@
+{-# LANGUAGE OverloadedStrings #-}
+
 module Octane.Type.ReplaySpec (spec) where
 
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.Map as Map
+import Octane
 import Test.Tasty.Hspec
 
 spec :: Spec
 spec = describe "Replay" $ do
-    return ()
+    it "can be decoded" $ do
+        shouldBe
+            (decodeReplay "\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\1\0\0\0\0\
+                \\5\0\0\0None\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \")
+            (Right ("", 78, Replay
+                (Int32LE 0)
+                (Int32LE 0)
+                (Int32LE 0)
+                (Int32LE 0)
+                (PCString "")
+                (Dictionary Map.empty)
+                (Int32LE 0)
+                (Int32LE 0)
+                (List [])
+                (List [])
+                ""
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])))
+    it "can be encoded" $ do
+        shouldBe
+            (Binary.encode (Replay
+                (Int32LE 0)
+                (Int32LE 0)
+                (Int32LE 0)
+                (Int32LE 0)
+                (PCString "")
+                (Dictionary Map.empty)
+                (Int32LE 0)
+                (Int32LE 0)
+                (List [])
+                (List [])
+                ""
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])
+                (List [])))
+            "\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\1\0\0\0\0\
+                \\5\0\0\0None\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \\0\0\0\0\
+                \"
+
+decodeReplay :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Replay)
+decodeReplay = Binary.decodeOrFail
