diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1,1 @@
+Copyright 2017 Alex Brandt <alunduil@alunduil.com>
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for siren-json
+
+## 0.1.0.0  -- 2017-11-08
+
+* First version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Alex Brandt
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# Description
+
+[Siren] Tools
+
+Types, classes, and functions for using [Siren][siren] in [Haskell].
+
+# Getting Started
+
+Documentation is available on [Hackage] and information about
+`application/vnd.siren+json` can be found at
+<https://github.com/kevinswiber/siren>.
+
+# Reporting Issues
+
+Any issues discovered should be recorded on [github][issues].  If you believe
+you've found an error or have a suggestion for a new feature; please, ensure
+that it is reported.
+
+If you would like to contribute a fix or new feature; please, submit a pull
+request.  This project follows [git flow] and utilizes [travis] to automatically
+check pull requests before a manual review.
+
+# Contributors
+
+The `COPYRIGHT` file contains a list of contributors with their respective
+copyrights and other information.  If you submit a pull request and would like
+attribution; please, add yourself to the `COPYRIGHT` file.
+
+[siren]: http://amundsen.com/media-types/collection/
+[git flow]: http://nvie.com/posts/a-successful-git-branching-model/
+[Hackage]: https://hackage.haskell.org/package/collection-json
+[Haskell]: https://www.haskell.org/
+[issues]: https://github.com/alunduil/siren-json.hs/issues
+[travis]: https://travis-ci.org/alunduil/siren-json.hs
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/siren-json.cabal b/siren-json.cabal
new file mode 100644
--- /dev/null
+++ b/siren-json.cabal
@@ -0,0 +1,111 @@
+name:                siren-json
+version:             0.1.0.0
+synopsis:            Siren Tools for Haskell
+
+description:         
+  Types, classes, and functions for using the Siren—Hypermedia Type in Haskell.
+
+homepage:            https://github.com/alunduil/siren-json.hs
+bug-reports:         https://github.com/alunduil/siren-json.hs/issues
+license:             MIT
+license-file:        LICENSE
+author:              Alex Brandt
+maintainer:          alunduil@alunduil.com
+copyright:           (c) 2017 Alex Brandt
+category:            Data
+build-type:          Simple
+cabal-version:       >= 1.10
+tested-with:         GHC >= 7.6 && < 9.0
+
+extra-source-files:
+    ChangeLog.md
+  , COPYRIGHT
+  , LICENSE
+  , README.md
+  , Setup.hs
+
+source-repository head
+  type:     git
+  location: https://github.com/alunduil/siren-json.git
+  branch:   develop
+
+library
+  default-language:    Haskell2010
+
+  ghc-options: -Wall -fwarn-tabs -fwarn-monomorphism-restriction
+               -fwarn-unused-do-bind
+
+  hs-source-dirs:
+      src
+
+  exposed-modules:
+      Data.SirenJSON
+
+  other-modules:
+      External.Network.HTTP.Media.MediaType.JSON
+    , External.Network.HTTP.Types.Method.JSON
+    , External.Network.URI.JSON
+
+  build-depends:
+      aeson                >= 0.8 && < 1.2
+    , base                 >= 4.6 && < 4.12
+    , bytestring           == 0.10.*
+    , containers           == 0.5.*
+    , http-media           == 0.6.*
+    , http-types           == 0.9.*
+    , network-uri          == 2.6.*
+    , text                 == 1.2.*
+    , unordered-containers == 0.2.*
+
+  other-extensions:
+
+test-suite siren-json-tests
+  default-language: Haskell2010
+  type:             exitcode-stdio-1.0
+  main-is:          Spec.hs
+
+  ghc-options: -Wall -fwarn-tabs -fwarn-monomorphism-restriction
+               -fwarn-unused-do-bind
+
+  hs-source-dirs:
+      src
+    , test
+
+  other-modules:
+      Data.SirenJSON
+    , Data.SirenJSON.Arbitrary
+    , Data.SirenJSON.Norm
+    , Data.SirenJSONSpec
+    , External.Network.HTTP.Media.MediaType.Arbitrary
+    , External.Network.HTTP.Media.MediaType.ArbitrarySpec
+    , External.Network.HTTP.Media.MediaType.JSON
+    , External.Network.HTTP.Media.MediaType.JSONSpec
+    , External.Network.HTTP.Types.Method.Arbitrary
+    , External.Network.HTTP.Types.Method.ArbitrarySpec
+    , External.Network.HTTP.Types.Method.JSON
+    , External.Network.HTTP.Types.Method.JSONSpec
+    , External.Network.URI.Arbitrary
+    , External.Network.URI.ArbitrarySpec
+    , External.Network.URI.JSON
+    , External.Network.URI.JSONSpec
+
+  build-tool-depends:
+      hspec-discover:hspec-discover == 2.4.*
+
+  build-depends:
+      aeson                >= 0.8 && < 1.2
+    , base                 >= 4.6 && < 4.12
+    , bytestring           == 0.10.*
+    , case-insensitive     == 1.2.*
+    , containers           == 0.5.*
+    , hspec                == 2.4.*
+    , http-media           == 0.6.*
+    , http-types           == 0.9.*
+    , network-uri          == 2.6.*
+    , QuickCheck           == 2.9.*
+    , quickcheck-instances == 0.3.*
+    , test-invariant       == 0.4.*
+    , text                 == 1.2.*
+    , unordered-containers == 0.2.*
+
+  other-extensions:
diff --git a/src/Data/SirenJSON.hs b/src/Data/SirenJSON.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/SirenJSON.hs
@@ -0,0 +1,263 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+{-|
+Module      : Data.SirenJSON
+Description : Types and Instances for @application/vnd.siren+json@
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+A collection of types and instances for @application/vnd.siren+json@.
+
+Full documentation for @application/vnd.siren+json@ can be found at
+<https://github.com/kevinswiber/siren>.
+-}
+module Data.SirenJSON where
+
+import Data.Aeson ((.=), (.:?), (.!=), (.:), FromJSON (parseJSON), Object, object, ToJSON (toJSON), Value (Object, String), withObject, withText)
+import Data.Foldable (asum)
+import Data.Functor ((<$>))
+import Data.Maybe (catMaybes)
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import Network.HTTP.Media (MediaType)
+import Network.HTTP.Types (StdMethod (GET))
+import Network.URI (URI)
+
+import qualified Data.HashMap.Lazy as HashMap (fromList)
+import qualified Data.Map.Strict as Map (empty, Map, null)
+
+import External.Network.HTTP.Media.MediaType.JSON ()
+import External.Network.HTTP.Types.Method.JSON ()
+import External.Network.URI.JSON ()
+
+-- * Core Data Types
+
+-- | The top-level object for an @application/vnd.siren+json@ resource.
+data Entity = Entity
+  { eClass      :: [Text]            -- ^ Nature of 'Entity''s
+                                     --   content---implementation dependant and
+                                     --   should be documented
+  , eProperties :: Map.Map Text Text -- ^ 'Entity' state as key-value pairs---we
+                                     --   encode all values as strings for
+                                     --   simplicity and to allow parsing into
+                                     --   other types as needed
+  , eEntities   :: [SubEntity]
+  , eLinks      :: [Link]
+  , eActions    :: [Action]
+  , eTitle      :: Maybe Text        -- ^ Descriptive text about 'Entity'
+  } deriving (Eq, Show)
+
+instance FromJSON Entity where
+  parseJSON = withObject "Entity" $ \ v -> do
+    eClass      <- v .:? "class"      .!= []
+    eProperties <- v .:? "properties" .!= Map.empty
+    eEntities   <- v .:? "entities"   .!= []
+    eLinks      <- v .:? "links"      .!= []
+    eActions    <- v .:? "actions"    .!= []
+    eTitle      <- v .:? "title"
+
+    return Entity{..}
+
+instance ToJSON Entity where
+  toJSON Entity{..} = object $ catMaybes
+    [ if null eClass          then Nothing else Just $ "class"      .= eClass
+    , if Map.null eProperties then Nothing else Just $ "properties" .= eProperties
+    , if null eEntities       then Nothing else Just $ "entities"   .= eEntities
+    , if null eLinks          then Nothing else Just $ "links"      .= eLinks
+    , if null eActions        then Nothing else Just $ "actions"    .= eActions
+    , (.=) "title" <$> eTitle
+    ]
+
+-- | Nested object for an @application/vnd.siren+json@.
+data SubEntity = EmbeddedLink Link
+               | EmbeddedRepresentation
+                   { sEntity :: Entity
+                   , sRel   :: [Text]
+                   }
+  deriving (Eq, Show)
+
+instance FromJSON SubEntity where
+  parseJSON = withObject "SubEntity" $ \ v -> asum
+    [ EmbeddedLink <$> parseJSON (Object v)
+    , do sEntity <- parseJSON (Object v)
+         sRel    <- v .: "rel"
+
+         return EmbeddedRepresentation{..}
+    ]
+
+instance ToJSON SubEntity where
+  toJSON (EmbeddedLink l) = toJSON l
+  toJSON EmbeddedRepresentation{..} = Object $ toObject sEntity <> HashMap.fromList ["rel" .= sRel]
+                                      where toObject :: ToJSON a => a -> Object
+                                            toObject v = case toJSON v of
+                                                           Object o -> o
+                                                           _        -> error "toObject: received non-Object"
+
+-- | Link to a related resource.
+data Link = Link
+  { lClass :: [Text]          -- ^ Nature of 'Entity''s content---
+                              --   implementation dependant and should be
+                              --   documented
+  , lRel   :: [Text]
+  , lHref  :: URI
+  , lType  :: Maybe MediaType
+  , lTitle :: Maybe Text
+  } deriving (Eq, Show)
+
+instance FromJSON Link where
+  parseJSON = withObject "Link" $ \ v -> do
+    lClass <- v .:? "class" .!= []
+    lRel   <- v .:  "rel"
+    lHref  <- v .:  "href"
+    lType  <- v .:? "type"
+    lTitle <- v .:? "title"
+
+    return Link{..}
+
+instance ToJSON Link where
+  toJSON Link{..} = object $ catMaybes
+    [ if null lClass then Nothing else Just $ "class" .= lClass
+    , Just $ "rel"  .= lRel
+    , Just $ "href" .= lHref
+    , (.=) "type"  <$> lType
+    , (.=) "title" <$> lTitle
+    ]
+
+-- | Behavior of an 'Entity'.
+data Action = Action
+  { aName   :: Text
+  , aClass  :: [Text]
+  , aMethod :: Maybe StdMethod
+  , aHref   :: URI
+  , aTitle  :: Maybe Text
+  , aType   :: Maybe MediaType
+  , aFields :: [Field]
+  } deriving (Eq, Show)
+
+instance FromJSON Action where
+  parseJSON = withObject "Action" $ \ v -> do
+    aName   <- v .:  "name"
+    aClass  <- v .:? "class"  .!= []
+    aMethod <- v .:? "method" .!= Just GET
+    aHref   <- v .:  "href"
+    aTitle  <- v .:? "title"
+    aType   <- v .:? "type"
+    aFields <- v .:? "fields" .!= []
+
+    return Action{..}
+
+instance ToJSON Action where
+  toJSON Action{..} = object $ catMaybes
+    [ Just $ "name" .= aName
+    , if null aClass then Nothing else Just $ "class"  .= aClass
+    , (.=) "method" <$> aMethod
+    , Just $ "href" .= aHref
+    , (.=) "title" <$> aTitle
+    , (.=) "type"  <$> (show <$> aType)
+    , if null aFields then Nothing else Just $ "fields" .= aFields
+    ]
+
+-- | Control inside of an 'Action'.
+data Field = Field
+  { fName  :: Text
+  , fClass :: [Text]
+  , fType  :: Maybe InputType
+  , fValue :: Maybe Text
+  , fTitle :: Maybe Text
+  } deriving (Eq, Show)
+
+instance FromJSON Field where
+  parseJSON = withObject "Field" $ \ v -> do
+    fName  <- v .:  "name"
+    fClass <- v .:? "class" .!= []
+    fType  <- v .:? "type"  .!= Just Text
+    fValue <- v .:? "value"
+    fTitle <- v .:? "title"
+
+    return Field{..}
+
+instance ToJSON Field where
+  toJSON Field{..} = object $ catMaybes
+    [ Just $ "name" .= fName
+    , if null fClass then Nothing else Just $ "class" .= fClass
+    , (.=) "type"  <$> fType
+    , (.=) "value" <$> fValue
+    , (.=) "title" <$> fTitle
+    ]
+
+data InputType = Hidden
+               | Text
+               | Search
+               | Tel
+               | URL
+               | Email
+               | Password
+               | DateTime
+               | Date
+               | Month
+               | Week
+               | Time
+               | DateTimeLocal
+               | Number
+               | Range
+               | Color
+               | CheckBox
+               | Radio
+               | File
+  deriving (Eq, Show)
+
+instance FromJSON InputType where
+  parseJSON = withText "InputType" $ \ v ->
+    case v of
+      "hidden"         -> return Hidden
+      "text"           -> return Text
+      "search"         -> return Search
+      "tel"            -> return Tel
+      "url"            -> return URL
+      "email"          -> return Email
+      "password"       -> return Password
+      "datetime"       -> return DateTime
+      "date"           -> return Date
+      "month"          -> return Month
+      "week"           -> return Week
+      "time"           -> return Time
+      "datetime-local" -> return DateTimeLocal
+      "number"         -> return Number
+      "range"          -> return Range
+      "color"          -> return Color
+      "checkbox"       -> return CheckBox
+      "radio"          -> return Radio
+      "file"           -> return File
+      _                -> fail "invalid InputType"
+    
+instance ToJSON InputType where
+  toJSON Hidden        = String "hidden"
+  toJSON Text          = String "text"
+  toJSON Search        = String "search"
+  toJSON Tel           = String "tel"
+  toJSON URL           = String "url"
+  toJSON Email         = String "email"
+  toJSON Password      = String "password"
+  toJSON DateTime      = String "datetime"
+  toJSON Date          = String "date"
+  toJSON Month         = String "month"
+  toJSON Week          = String "week"
+  toJSON Time          = String "time"
+  toJSON DateTimeLocal = String "datetime-local"
+  toJSON Number        = String "number"
+  toJSON Range         = String "range"
+  toJSON Color         = String "color"
+  toJSON CheckBox      = String "checkbox"
+  toJSON Radio         = String "radio"
+  toJSON File          = String "file"
+
+-- * Type Conversion
+
+-- | A type that can be converted from 'Entity'.
+class FromEntity a where
+  fromEntity :: Entity -> a
+
+-- | A type that can be converted to 'Entity'.
+class ToEntity a where
+  toEntity :: a -> Entity
diff --git a/src/External/Network/HTTP/Media/MediaType/JSON.hs b/src/External/Network/HTTP/Media/MediaType/JSON.hs
new file mode 100644
--- /dev/null
+++ b/src/External/Network/HTTP/Media/MediaType/JSON.hs
@@ -0,0 +1,22 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : External.Network.HTTP.Media.MediaType.JSON
+Description : MediaType FromJSON and ToJSON Instances
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+MediaType Instances for FromJSON and ToJSON.
+-}
+module External.Network.HTTP.Media.MediaType.JSON where
+
+import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), withText)
+import Data.String (fromString)
+import Data.Text (pack, unpack)
+import Network.HTTP.Media.MediaType (MediaType)
+
+instance FromJSON MediaType where
+  parseJSON = withText "MediaType" $ return . fromString . unpack
+
+instance ToJSON MediaType where
+  toJSON = toJSON . pack . show
diff --git a/src/External/Network/HTTP/Types/Method/JSON.hs b/src/External/Network/HTTP/Types/Method/JSON.hs
new file mode 100644
--- /dev/null
+++ b/src/External/Network/HTTP/Types/Method/JSON.hs
@@ -0,0 +1,22 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : External.Network.HTTP.Types.Method.JSON
+Description : Method FromJSON and ToJSON Instances
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Method Instances for FromJSON and ToJSON.
+-}
+module External.Network.HTTP.Types.Method.JSON where
+
+import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), withText)
+import Data.ByteString.Char8 (unpack)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Network.HTTP.Types.Method (StdMethod, parseMethod, renderStdMethod)
+
+instance FromJSON StdMethod where
+  parseJSON = withText "StdMethod" $ either (fail . unpack) return . parseMethod . encodeUtf8
+
+instance ToJSON StdMethod where
+  toJSON = toJSON . decodeUtf8 . renderStdMethod
diff --git a/src/External/Network/URI/JSON.hs b/src/External/Network/URI/JSON.hs
new file mode 100644
--- /dev/null
+++ b/src/External/Network/URI/JSON.hs
@@ -0,0 +1,23 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : External.Network.URI.JSON
+Description : URI FromJSON and ToJSON Instances
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+URI Instances for FromJSON and ToJSON
+-}
+module External.Network.URI.JSON where
+
+import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), withText)
+import Data.Text (unpack)
+import Network.URI (parseURIReference, URI, uriToString)
+
+instance FromJSON URI where
+  parseJSON = withText "URI" $ maybe (fail "invalid URI") return . parseURIReference . unpack
+
+instance ToJSON URI where
+  toJSON u = toJSON $ uriToString id u ""
diff --git a/test/Data/SirenJSON/Arbitrary.hs b/test/Data/SirenJSON/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/SirenJSON/Arbitrary.hs
@@ -0,0 +1,79 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : Data.SirenJSON.Arbitrary
+Description : Arbitrary Instances for Data.SirenJSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Arbitrary instances for "Data.SirenJSON".
+-}
+module Data.SirenJSON.Arbitrary where
+
+import Control.Applicative ((<$>), (<*>))
+import Test.QuickCheck (Arbitrary (arbitrary), elements, oneof, scale)
+import Test.QuickCheck.Instances ()
+
+import Data.SirenJSON
+import External.Network.HTTP.Media.MediaType.Arbitrary ()
+import External.Network.HTTP.Types.Method.Arbitrary ()
+import External.Network.URI.Arbitrary ()
+
+instance Arbitrary Entity where
+  arbitrary = Entity <$> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+
+instance Arbitrary SubEntity where
+  arbitrary = oneof [ EmbeddedLink <$> arbitrary
+                    , EmbeddedRepresentation <$> scale (`div` 2) arbitrary
+                                             <*> arbitrary
+                    ]
+
+instance Arbitrary Link where
+  arbitrary = Link <$> arbitrary
+                   <*> arbitrary
+                   <*> arbitrary
+                   <*> arbitrary
+                   <*> arbitrary
+
+instance Arbitrary Action where
+  arbitrary = Action <$> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+                     <*> arbitrary
+
+instance Arbitrary Field where
+  arbitrary = Field <$> arbitrary
+                    <*> arbitrary
+                    <*> arbitrary
+                    <*> arbitrary
+                    <*> arbitrary
+
+instance Arbitrary InputType where
+  arbitrary = elements [ Hidden
+                       , Text
+                       , Search
+                       , Tel
+                       , URL
+                       , Email
+                       , Password
+                       , DateTime
+                       , Date
+                       , Month
+                       , Week
+                       , Time
+                       , DateTimeLocal
+                       , Number
+                       , Range
+                       , Color
+                       , CheckBox
+                       , Radio
+                       , File
+                       ]
diff --git a/test/Data/SirenJSON/Norm.hs b/test/Data/SirenJSON/Norm.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/SirenJSON/Norm.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE RecordWildCards #-}
+
+{-|
+Module      : Data.SirenJSON.Norm
+Description : Norm Instances for Data.SirenJSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Norm instances for "Data.SirenJSON".
+
+Also includes a definition for the 'Norm' class until it is proven generally
+useful.
+-}
+module Data.SirenJSON.Norm
+  ( Norm (..)
+  ) where
+
+import Data.Functor ((<$>))
+import Data.Maybe (isNothing)
+import Network.HTTP.Types.Method (StdMethod (GET))
+
+import Data.SirenJSON
+
+class Norm a where
+  normalize :: a -> a
+  normalize = id
+
+instance Norm Entity where
+  normalize Entity{..} = Entity eClass
+                                eProperties
+                                (normalize <$> eEntities)
+                                (normalize <$> eLinks)
+                                (normalize <$> eActions)
+                                eTitle
+
+instance Norm SubEntity where
+  normalize (EmbeddedLink l) = EmbeddedLink $ normalize l
+  normalize EmbeddedRepresentation{..} = EmbeddedRepresentation (normalize sEntity) sRel
+
+instance Norm Link
+
+instance Norm Action where
+  normalize Action{..} = Action aName
+                                aClass
+                                (if isNothing aMethod then Just GET else aMethod)
+                                aHref
+                                aTitle
+                                aType
+                                (normalize <$> aFields)
+
+instance Norm Field where
+  normalize Field{..} = Field fName
+                              fClass
+                              (if isNothing fType then Just Text else fType)
+                              fValue
+                              fTitle
diff --git a/test/Data/SirenJSONSpec.hs b/test/Data/SirenJSONSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/SirenJSONSpec.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+{-|
+Module      : Data.SirenJSONSpec
+Description : Tests for Data.SirenJSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "Data.SirenJSON".
+-}
+module Data.SirenJSONSpec (main, spec) where
+
+import Data.Aeson (decode, encode)
+import Data.Maybe (fromJust, isJust)
+import Network.URI (parseURIReference)
+import Test.Hspec (context, describe, hspec, it, shouldBe, Spec)
+import Test.Hspec.QuickCheck (modifyMaxSize, prop)
+import Test.Invariant ((<=>))
+import Test.QuickCheck.Instances ()
+
+import qualified Data.ByteString.Lazy as BL (ByteString)
+import qualified Data.Map.Strict as Map (empty)
+
+import Data.SirenJSON
+import Data.SirenJSON.Arbitrary ()
+import Data.SirenJSON.Norm (Norm (normalize))
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "application/vnd.siren+json" $ modifyMaxSize (const 25) $
+    do describe "properties" $
+         context "fromJust . decode . encode == normalize" $
+           do prop "Field"     (fromJust . decode . encode <=> normalize :: Field -> Bool)
+              prop "Action"    (fromJust . decode . encode <=> normalize :: Action -> Bool)
+              prop "Link"      (fromJust . decode . encode <=> normalize :: Link -> Bool)
+              prop "SubEntity" (fromJust . decode . encode <=> normalize :: SubEntity -> Bool)
+              prop "Entity"    (fromJust . decode . encode <=> normalize :: Entity -> Bool)
+
+       describe "differentiate SubEntity values" $
+         do it "SubEntity_EmbeddedRepresentation" $
+              (decode mSubEntity_EmbeddedRepresentation :: Maybe SubEntity) `shouldBe` Just (EmbeddedRepresentation (Entity [] Map.empty [] [] [] Nothing) [])
+
+            it "SubEntity_EmbeddedLink" $
+              (decode mSubEntity_EmbeddedLink :: Maybe SubEntity) `shouldBe` Just (EmbeddedLink (Link [] [] eURI Nothing Nothing))
+
+       describe "JSON Missing Keys" $
+         do context "decode minimal JSON strings" $
+              do it "Field"                            $ isJust (decode mField :: Maybe Field)
+                 it "Action"                           $ isJust (decode mAction :: Maybe Action)
+                 it "Link"                             $ isJust (decode mLink :: Maybe Link)
+                 it "SubEntity_EmbeddedRepresentation" $ isJust (decode mSubEntity_EmbeddedRepresentation :: Maybe SubEntity)
+                 it "SubEntity_EmbeddedLink"           $ isJust (decode mSubEntity_EmbeddedLink :: Maybe SubEntity)
+                 it "Entity"                           $ isJust (decode mEntity :: Maybe Entity)
+
+            context "encode minimal data to JSON" $
+              do it "Field" $
+                   encode (Field "name" [] Nothing Nothing Nothing) `shouldBe` mField
+
+                 it "Action" $
+                   encode (Action "name" [] Nothing eURI Nothing Nothing []) `shouldBe` mAction
+
+                 it "Link" $
+                   encode (Link [] [] eURI Nothing Nothing) `shouldBe` mLink
+
+                 it "SubEntity_EmbeddedRepresentation" $
+                   encode (EmbeddedRepresentation (Entity [] Map.empty [] [] [] Nothing) []) `shouldBe` mSubEntity_EmbeddedRepresentation
+
+                 it "SubEntity_EmbeddedLink" $
+                   encode (EmbeddedLink (Link [] [] eURI Nothing Nothing)) `shouldBe` mSubEntity_EmbeddedLink
+
+                 it "Entity" $
+                   encode (Entity [] Map.empty [] [] [] Nothing) `shouldBe` mEntity
+
+  where mEntity                           = "{}" :: BL.ByteString
+        mSubEntity_EmbeddedLink           = "{\"href\":\"http://example.com\",\"rel\":[]}" :: BL.ByteString
+        mSubEntity_EmbeddedRepresentation = "{\"rel\":[]}" :: BL.ByteString
+        mLink                             = "{\"href\":\"http://example.com\",\"rel\":[]}" :: BL.ByteString
+        mAction                           = "{\"href\":\"http://example.com\",\"name\":\"name\"}" :: BL.ByteString
+        mField                            = "{\"name\":\"name\"}" :: BL.ByteString
+        
+        eURI = fromJust $ parseURIReference "http://example.com"
diff --git a/test/External/Network/HTTP/Media/MediaType/Arbitrary.hs b/test/External/Network/HTTP/Media/MediaType/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Media/MediaType/Arbitrary.hs
@@ -0,0 +1,58 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : External.Network.HTTP.Media.MediaType.Arbitrary
+Description : Arbitrary Instances for Network.HTTP.Media.MediaType
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Arbitrary instances for "Network.HTTP.Media.MediaType".
+-}
+module External.Network.HTTP.Media.MediaType.Arbitrary () where
+
+import Prelude hiding (concat)
+
+import Control.Applicative ((<*>))
+import Control.Monad (replicateM)
+import Data.ByteString (append, concat, ByteString)
+import Data.ByteString.Char8 (singleton)
+import Data.Functor ((<$>))
+import Network.HTTP.Media.MediaType ((/:), (//), MediaType)
+import Test.QuickCheck (Arbitrary (arbitrary), choose, elements, Gen, listOf, oneof, sized)
+
+--
+--
+-- Note: parameter---paramter values are supposed to be unrestricted but due to
+--       the way that quickcheck validates values it's best if these are
+--       printable.  Until we can use the new instances in quickcheck-2.10.*, we
+--       shall simply use restrictedName for the values as well.
+instance Arbitrary MediaType where
+  arbitrary =
+    do n  <- (//) <$> restrictedName <*> restrictedName
+       ps <- listOf $ (,) <$> restrictedName <*> restrictedName -- see parameter note above
+       return $ foldl (/:) n ps
+
+-- * RFC 6838 Generators
+
+restrictedName :: Gen ByteString
+restrictedName = sized $ \ s ->
+  do n  <- choose (0, min 126 s)
+     rs <- concat <$> replicateM n restrictedNameChar
+     (`append` rs) <$> restrictedNameFirst
+
+restrictedNameFirst :: Gen ByteString
+restrictedNameFirst = singleton <$> oneof [alpha, digit]
+
+restrictedNameChar :: Gen ByteString
+restrictedNameChar = singleton <$> oneof [ alpha
+                                         , digit
+                                         , elements ['!', '#', '$', '&', '-', '^', '_', '.', '+']
+                                         ]
+
+-- * RFC 2234 Generators
+
+alpha :: Gen Char
+alpha = elements $ ['a'..'z'] ++ ['A'..'Z']
+
+digit :: Gen Char
+digit = elements ['0'..'9']
diff --git a/test/External/Network/HTTP/Media/MediaType/ArbitrarySpec.hs b/test/External/Network/HTTP/Media/MediaType/ArbitrarySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Media/MediaType/ArbitrarySpec.hs
@@ -0,0 +1,28 @@
+{-|
+Module      : External.Network.HTTP.Media.MediaType.ArbitrarySpec
+Description : Tests for External.Network.HTTP.Media.MediaType.Arbitrary
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.HTTP.Media.MediaType.Arbitrary".
+-}
+module External.Network.HTTP.Media.MediaType.ArbitrarySpec (main, spec) where
+
+import Prelude hiding (null)
+
+import Data.ByteString (null)
+import Data.CaseInsensitive (original)
+import Network.HTTP.Media.MediaType (mainType, subType)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+
+import External.Network.HTTP.Media.MediaType.Arbitrary ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    do prop "not . null . mainType" $ not . null . original . mainType
+       prop "not . null . subType"  $ not . null . original . subType
diff --git a/test/External/Network/HTTP/Media/MediaType/JSONSpec.hs b/test/External/Network/HTTP/Media/MediaType/JSONSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Media/MediaType/JSONSpec.hs
@@ -0,0 +1,27 @@
+{-|
+Module      : External.Network.HTTP.Media.MediaType.JSONSpec
+Description : Tests for External.Network.HTTP.Media.MediaType.JSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.HTTP.Media.MediaType.JSON".
+-}
+module External.Network.HTTP.Media.MediaType.JSONSpec (main, spec) where
+
+import Data.Aeson (decode, encode)
+import Data.Maybe (fromJust)
+import Network.HTTP.Media.MediaType (MediaType)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+import Test.Invariant ((<=>))
+
+import External.Network.HTTP.Media.MediaType.Arbitrary ()
+import External.Network.HTTP.Media.MediaType.JSON ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    prop "fromJust . decode . encode == id" (fromJust . decode . encode <=> id :: MediaType -> Bool)
diff --git a/test/External/Network/HTTP/Types/Method/Arbitrary.hs b/test/External/Network/HTTP/Types/Method/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Types/Method/Arbitrary.hs
@@ -0,0 +1,26 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : External.Network.HTTP.Types.Method.Arbitrary
+Description : Arbitrary Instances for Network.HTTP.Types.Method
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Arbitrary instances for "Network.HTTP.Types.Method".
+-}
+module External.Network.HTTP.Types.Method.Arbitrary () where
+
+import Network.HTTP.Types.Method (StdMethod (..))
+import Test.QuickCheck (Arbitrary (arbitrary), elements)
+
+instance Arbitrary StdMethod where
+  arbitrary = elements [ GET
+                       , POST
+                       , HEAD
+                       , PUT
+                       , DELETE
+                       , TRACE
+                       , CONNECT
+                       , OPTIONS
+                       , PATCH
+                       ]
diff --git a/test/External/Network/HTTP/Types/Method/ArbitrarySpec.hs b/test/External/Network/HTTP/Types/Method/ArbitrarySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Types/Method/ArbitrarySpec.hs
@@ -0,0 +1,24 @@
+{-|
+Module      : External.Network.HTTP.Types.Method.ArbitrarySpec
+Description : Tests for External.Network.HTTP.Types.Method.Arbitrary
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.HTTP.Types.Method.Arbitrary".
+-}
+module External.Network.HTTP.Types.Method.ArbitrarySpec (main, spec) where
+
+import Network.HTTP.Types.Method (parseMethod, renderStdMethod)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+import Test.Invariant ((<=>))
+
+import External.Network.HTTP.Types.Method.Arbitrary ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    prop "parseMethod . renderStdMethod <=> Right" $ parseMethod . renderStdMethod <=> Right
diff --git a/test/External/Network/HTTP/Types/Method/JSONSpec.hs b/test/External/Network/HTTP/Types/Method/JSONSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/HTTP/Types/Method/JSONSpec.hs
@@ -0,0 +1,27 @@
+{-|
+Module      : External.Network.HTTP.Types.Method.JSONSpec
+Description : Tests for External.Network.HTTP.Types.Method.JSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.HTTP.Types.Method.JSON".
+-}
+module External.Network.HTTP.Types.Method.JSONSpec (main, spec) where
+
+import Data.Aeson (decode, encode)
+import Data.Maybe (fromJust)
+import Network.HTTP.Types.Method (StdMethod)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+import Test.Invariant ((<=>))
+
+import External.Network.HTTP.Types.Method.Arbitrary ()
+import External.Network.HTTP.Types.Method.JSON ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    prop "fromJust . decode . encode == id" (fromJust . decode . encode <=> id :: StdMethod -> Bool)
diff --git a/test/External/Network/URI/Arbitrary.hs b/test/External/Network/URI/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/URI/Arbitrary.hs
@@ -0,0 +1,187 @@
+{-# LANGUAGE RecordWildCards #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-|
+Module      : External.Network.URI.Arbitrary
+Description : Arbitrary Instances for Network.URI
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Arbitrary instances for "Network.URI".
+-}
+module External.Network.URI.Arbitrary () where
+
+import Control.Applicative ((<$>), (<*>))
+import Control.Monad (replicateM)
+import Data.List (intercalate)
+import Network.URI (URI (..), URIAuth (..))
+import Test.QuickCheck (Arbitrary (arbitrary), choose, elements, Gen, listOf, listOf1, oneof, suchThat)
+
+instance Arbitrary URI where
+  arbitrary =
+    do uriScheme    <- scheme
+       uriAuthority <- arbitrary :: Gen (Maybe URIAuth)
+       uriPath      <- path (null uriScheme) $ maybe True emptyAuthority uriAuthority
+       uriQuery     <- oneof [query, return ""]
+       uriFragment  <- oneof [fragment, return ""]
+
+       return URI {..}
+    where emptyAuthority URIAuth{..} = all null [uriUserInfo, uriRegName, uriPort]
+
+instance Arbitrary URIAuth where
+  arbitrary = URIAuth <$> userinfo
+                      <*> host `suchThat` (not . null)
+                      <*> port
+
+-- * RFC 3986 Generators
+--
+--   Some generators are handled by the 'Arbitrary' instances above, and others
+--   are folded into symbols that are preceeded or followed by identifying
+--   tokens.
+
+scheme :: Gen String
+scheme =
+  do a <- alpha
+     r <- listOf $ oneof [alpha, digit, elements ['+', '-', '.']]
+     return $ a : (r ++ ":")
+
+userinfo :: Gen String
+userinfo =
+  do u <- concat <$> userinfo'
+     if null u
+        then return ""
+        else return $ u ++ "@"
+  where userinfo' = listOf $ oneof [ replicateM 1 $ oneof [unreserved, subDelims, return ':']
+                                   , percentEncoded
+                                   ]
+
+host :: Gen String
+host = oneof [ ipLiteral
+             , ipv4Address
+             , regName
+             ]
+
+port :: Gen String
+port =
+  do p <- listOf digit
+     if null p
+        then return ""
+        else return $ ':':p
+
+ipLiteral :: Gen String
+ipLiteral =
+  do x <- oneof [ ipv6Address
+                --, ipvFuture
+                ]
+     return $ "[" ++ x ++ "]"
+
+{- TODO Check that "Network.URI" implements this correctly.
+ipvFuture :: Gen String
+ipvFuture =
+  do h <- hexdig
+     o <- oneof [ unreserved, subDelims, return ':' ]
+     return ['v', h, '.', o]
+-}
+
+ipv6Address :: Gen String
+ipv6Address = concat <$> oneof [ sequence                        [b 6, ls32]
+                               , sequence           [return "::", b 5, ls32]
+                               , sequence      [h16, return "::", b 4, ls32]
+                               , sequence [b 1, h16, return "::", b 3, ls32]
+                               , sequence [b 2, h16, return "::", b 2, ls32]
+                               , sequence [b 3, h16, return "::", b 1, ls32]
+                               , sequence [b 4, h16, return "::",      ls32]
+                               , sequence [b 5, h16, return "::",       h16]
+                               , sequence [b 6, h16, return "::"]
+                               ]
+  where b n = fmap concat $ replicateM n $ fmap (++ ":") h16 :: Gen String
+
+h16 :: Gen String
+h16 = replicateM 4 hexdig
+
+ls32 :: Gen String
+ls32 = oneof [ intercalate ":" <$> replicateM 2 h16
+             , ipv4Address
+             ]
+
+ipv4Address :: Gen String
+ipv4Address = intercalate "." <$> replicateM 4 decOctet
+
+decOctet :: Gen String
+decOctet = (show :: Int -> String) <$> choose (0, 255)
+
+regName :: Gen String
+regName = fmap concat $ listOf $ oneof [ replicateM 1 unreserved
+                                       , percentEncoded
+                                       , replicateM 1 subDelims
+                                       ]
+
+path :: Bool -> Bool -> Gen String
+path emptyScheme emptyURIAuth = if emptyURIAuth
+                                   then oneof [ pathAbsolute
+                                              , if emptyScheme then pathNoScheme else pathRootless
+                                              , return ""
+                                              ]
+                                   else pathAbEmpty
+
+pathAbEmpty :: Gen String
+pathAbEmpty = concat <$> listOf ((('/':) . concat) <$> listOf pchar)
+
+pathAbsolute :: Gen String
+pathAbsolute = ('/':) <$> oneof [return "", pathRootless]
+
+pathNoScheme :: Gen String
+pathNoScheme = concat <$> sequence [segment1nc, pathAbEmpty]
+
+pathRootless :: Gen String
+pathRootless = concat <$> sequence [ concat <$> listOf1 pchar
+                                   , pathAbEmpty
+                                   ]
+
+segment1nc :: Gen String
+segment1nc = oneof [ replicateM 1 unreserved
+                   , percentEncoded
+                   , replicateM 1 subDelims
+                   , replicateM 1 $ return '@'
+                   ] 
+
+pchar :: Gen String
+pchar = oneof [ replicateM 1 unreserved
+              , percentEncoded
+              , replicateM 1 subDelims
+              , replicateM 1 $ return ':'
+              , replicateM 1 $ return '@'
+              ]
+
+query :: Gen String
+query = fmap (('?':) . concat) $ listOf $ oneof [ pchar
+                                                , return "/"
+                                                , return "?"
+                                                ]
+
+fragment :: Gen String
+fragment = fmap (('#':) . concat) $ listOf $ oneof [ pchar
+                                                   , return "/"
+                                                   , return "?"
+                                                   ]
+
+percentEncoded :: Gen String
+percentEncoded = ('%':) <$> replicateM 2 hexdig
+
+unreserved :: Gen Char
+unreserved = oneof [ alpha, digit, elements ['-', '.', '_', '~']]
+
+subDelims :: Gen Char
+subDelims = elements ['!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=']
+
+-- * RFC 2234 Generators
+
+alpha :: Gen Char
+alpha = elements $ ['a'..'z'] ++ ['A'..'Z']
+
+digit :: Gen Char
+digit = elements ['0'..'9']
+
+hexdig :: Gen Char
+hexdig = oneof [digit, elements ['A'..'F']]
diff --git a/test/External/Network/URI/ArbitrarySpec.hs b/test/External/Network/URI/ArbitrarySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/URI/ArbitrarySpec.hs
@@ -0,0 +1,25 @@
+{-|
+Module      : External.Network.URI.ArbitrarySpec
+Description : Tests for External.Network.URI.Arbitrary
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.URI.Arbitrary".
+-}
+module External.Network.URI.ArbitrarySpec (main, spec) where
+
+import Network.URI (isURIReference, parseURIReference, uriToString)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+
+import External.Network.URI.Arbitrary ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    do prop "isURIReference (uriToString id u \"\")" $ \ u -> isURIReference (uriToString id u "")
+
+       prop "Just u == parseURIReference (uriToString id u \"\")" $ \ u -> Just u == parseURIReference (uriToString id u "")
diff --git a/test/External/Network/URI/JSONSpec.hs b/test/External/Network/URI/JSONSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/External/Network/URI/JSONSpec.hs
@@ -0,0 +1,27 @@
+{-|
+Module      : External.Network.URI.JSONSpec
+Description : Tests for External.Network.URI.JSON
+Copyright   : (c) Alex Brandt, 2017
+License     : MIT
+
+Tests for "External.Network.URI.JSON".
+-}
+module External.Network.URI.JSONSpec (main, spec) where
+
+import Data.Aeson (decode, encode)
+import Data.Maybe (fromJust)
+import Network.URI (URI)
+import Test.Hspec (describe, hspec, Spec)
+import Test.Hspec.QuickCheck (prop)
+import Test.Invariant ((<=>))
+
+import External.Network.URI.Arbitrary ()
+import External.Network.URI.JSON ()
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+  describe "properties" $
+    prop "fromJust . decode . encode == id" (fromJust . decode . encode <=> id :: URI -> Bool)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
