diff --git a/.gitignore b/.gitignore
deleted file mode 100644
--- a/.gitignore
+++ /dev/null
@@ -1,30 +0,0 @@
-# General
-\#*#
-.*~
-*~
-.#*
-*.swp
-.DS_Store
-.gdb_history
-TAGS
-# Object files, etc
-*.a
-*.o
-*.so
-*.hi
-*.p_hi
-*.chi
-*.chs.h
-*.tix
-.hpc
-a.out
-# autotool
-autom4te.cache
-stamp-h1
-# misc
-*.sqlite
-Main
-dist/
-cabal-dev/
-.cabal-sandbox/
-cabal.sandbox.config
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
--- a/.travis.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-language: haskell
-before_install:
-    - cabal install hpc-coveralls
-script:
-    - cabal configure --enable-tests --enable-library-coverage && cabal build
-    - run-cabal-test
-after_script:
-    - hpc-coveralls tests
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+twitter-types 
+=============
+
+![CI](https://github.com/himura/twitter-types/workflows/CI/badge.svg)
+[![Hackage](https://img.shields.io/hackage/v/twitter-types.svg?style=flat)](https://hackage.haskell.org/package/twitter-types)
+[![Hackage-Deps](https://img.shields.io/hackage-deps/v/twitter-types.svg)](http://packdeps.haskellers.com/feed?needle=twitter-types)
+
+This library treats the Twitter JSON API in the Haskell way.
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,3 +1,5 @@
 #!/usr/bin/env runhaskell
+
 import Distribution.Simple
+
 main = defaultMain
diff --git a/Web/Twitter/Types.hs b/Web/Twitter/Types.hs
--- a/Web/Twitter/Types.hs
+++ b/Web/Twitter/Types.hs
@@ -1,497 +1,1166 @@
-{-# LANGUAGE OverloadedStrings, DeriveDataTypeable #-}
-
-module Web.Twitter.Types
-       ( DateString
-       , UserId
-       , Friends
-       , URIString
-       , UserName
-       , StatusId
-       , LanguageCode
-       , StreamingAPI(..)
-       , Status(..)
-       , SearchResult(..)
-       , SearchStatus(..)
-       , SearchMetadata(..)
-       , RetweetedStatus(..)
-       , DirectMessage(..)
-       , EventTarget(..)
-       , Event(..)
-       , Delete(..)
-       , User(..)
-       , List(..)
-       , Entities(..)
-       , EntityIndices
-       , Entity(..)
-       , HashTagEntity(..)
-       , UserEntity(..)
-       , URLEntity(..)
-       , MediaEntity(..)
-       , MediaSize(..)
-       , Coordinates(..)
-       , Place(..)
-       , BoundingBox(..)
-       , checkError
-       )
-       where
-
-import Data.Aeson
-import Data.Aeson.Types (Parser)
-import Data.Text (Text)
-import Data.HashMap.Strict (HashMap)
-import Control.Applicative
-import Control.Monad
-
-type DateString   = String
-type UserId       = Integer
-type Friends      = [UserId]
-type URIString    = Text
-type UserName     = Text
-type StatusId     = Integer
-type LanguageCode = String
-
-data StreamingAPI = SStatus Status
-                  | SRetweetedStatus RetweetedStatus
-                  | SEvent Event
-                  | SDelete Delete
-                  -- -- | SScrubGeo ScrubGeo
-                  | SFriends Friends
-                  | SUnknown Value
-                  deriving (Show, Eq)
-
-checkError :: Object -> Parser ()
-checkError o = do
-  err <- o .:? "error"
-  case err of
-    Just msg -> fail msg
-    Nothing -> return ()
-
-instance FromJSON StreamingAPI where
-  parseJSON v@(Object o) =
-    SRetweetedStatus <$> js <|>
-    SStatus <$> js <|>
-    SEvent <$> js <|>
-    SDelete <$> js <|>
-    SFriends <$> (o .: "friends") <|>
-    return (SUnknown v)
-    where
-      js :: FromJSON a => Parser a
-      js = parseJSON v
-  parseJSON _ = mzero
-
-data Status =
-  Status
-  { statusCreatedAt          :: DateString
-  , statusId                 :: StatusId
-  , statusText               :: Text
-  , statusSource             :: Text
-  , statusTruncated          :: Bool
-  , statusEntities           :: Maybe Entities
-  , statusExtendedEntities   :: Maybe Entities
-  , statusInReplyTo          :: Maybe StatusId
-  , statusInReplyToUser      :: Maybe UserId
-  , statusFavorite           :: Maybe Bool
-  , statusRetweetCount       :: Maybe Integer
-  , statusUser               :: User
-  , statusRetweet            :: Maybe Status
-  , statusPlace              :: Maybe Place
-  , statusFavoriteCount      :: Integer
-  , statusLang               :: Maybe Text
-  , statusPossiblySensitive  :: Maybe Bool
-  , statusCoordinates        :: Maybe Coordinates
-  } deriving (Show, Eq)
-
-instance FromJSON Status where
-  parseJSON (Object o) = checkError o >>
-    Status <$> o .:  "created_at"
-           <*> o .:  "id"
-           <*> o .:  "text"
-           <*> o .:  "source"
-           <*> o .:  "truncated"
-           <*> o .:? "entities"
-           <*> o .:? "extended_entities"
-           <*> o .:? "in_reply_to_status_id"
-           <*> o .:? "in_reply_to_user_id"
-           <*> o .:? "favorited"
-           <*> o .:? "retweet_count"
-           <*> o .:  "user"
-           <*> o .:? "retweeted_status"
-           <*> o .:? "place"
-           <*> o .:? "favorite_count" .!= 0
-           <*> o .:? "lang"
-           <*> o .:? "possibly_sensitive"
-           <*> o .:? "coordinates"
-  parseJSON _ = mzero
-
-data SearchResult body =
-  SearchResult
-  { searchResultStatuses :: body
-  , searchResultSearchMetadata :: SearchMetadata
-  } deriving (Show, Eq)
-
-instance FromJSON body =>
-         FromJSON (SearchResult body) where
-  parseJSON (Object o) = checkError o >>
-    SearchResult <$> o .:  "statuses"
-                 <*> o .:  "search_metadata"
-  parseJSON _ = mzero
-
-data SearchStatus =
-  SearchStatus
-  { searchStatusCreatedAt     :: DateString
-  , searchStatusId            :: StatusId
-  , searchStatusText          :: Text
-  , searchStatusSource        :: Text
-  , searchStatusUser          :: User
-  , searchStatusCoordinates   :: Maybe Coordinates
-  } deriving (Show, Eq)
-
-instance FromJSON SearchStatus where
-  parseJSON (Object o) = checkError o >>
-    SearchStatus <$> o .:  "created_at"
-                 <*> o .:  "id"
-                 <*> o .:  "text"
-                 <*> o .:  "source"
-                 <*> o .:  "user"
-                 <*> o .:? "coordinates"
-  parseJSON _ = mzero
-
-data SearchMetadata =
-  SearchMetadata
-  { searchMetadataMaxId         :: StatusId
-  , searchMetadataSinceId       :: StatusId
-  , searchMetadataRefreshUrl    :: URIString
-  , searchMetadataNextResults   :: Maybe URIString
-  , searchMetadataCount         :: Int
-  , searchMetadataCompletedIn   :: Maybe Float
-  , searchMetadataSinceIdStr    :: String
-  , searchMetadataQuery         :: String
-  , searchMetadataMaxIdStr      :: String
-  } deriving (Show, Eq)
-
-instance FromJSON SearchMetadata where
-  parseJSON (Object o) = checkError o >>
-    SearchMetadata <$> o .:  "max_id"
-                   <*> o .:  "since_id"
-                   <*> o .:  "refresh_url"
-                   <*> o .:? "next_results"
-                   <*> o .:  "count"
-                   <*> o .:? "completed_in"
-                   <*> o .:  "since_id_str"
-                   <*> o .:  "query"
-                   <*> o .:  "max_id_str"
-  parseJSON _ = mzero
-
-data RetweetedStatus =
-  RetweetedStatus
-  { rsCreatedAt       :: DateString
-  , rsId              :: StatusId
-  , rsText            :: Text
-  , rsSource          :: Text
-  , rsTruncated       :: Bool
-  , rsEntities        :: Maybe Entities
-  , rsUser            :: User
-  , rsRetweetedStatus :: Status
-  , rsCoordinates     :: Maybe Coordinates
-  } deriving (Show, Eq)
-
-instance FromJSON RetweetedStatus where
-  parseJSON (Object o) = checkError o >>
-    RetweetedStatus <$> o .:  "created_at"
-                    <*> o .:  "id"
-                    <*> o .:  "text"
-                    <*> o .:  "source"
-                    <*> o .:  "truncated"
-                    <*> o .:? "entities"
-                    <*> o .:  "user"
-                    <*> o .:  "retweeted_status"
-                    <*> o .:? "coordinates"
-  parseJSON _ = mzero
-
-data DirectMessage =
-  DirectMessage
-  { dmCreatedAt          :: DateString
-  , dmSenderScreenName   :: Text
-  , dmSender             :: User
-  , dmText               :: Text
-  , dmRecipientScreeName :: Text
-  , dmId                 :: StatusId
-  , dmRecipient          :: User
-  , dmRecipientId        :: UserId
-  , dmSenderId           :: UserId
-  , dmCoordinates        :: Maybe Coordinates
-  } deriving (Show, Eq)
-
-instance FromJSON DirectMessage where
-  parseJSON (Object o) = checkError o >>
-    DirectMessage <$> o .:  "created_at"
-                  <*> o .:  "sender_screen_name"
-                  <*> o .:  "sender"
-                  <*> o .:  "text"
-                  <*> o .:  "recipient_screen_name"
-                  <*> o .:  "id"
-                  <*> o .:  "recipient"
-                  <*> o .:  "recipient_id"
-                  <*> o .:  "sender_id"
-                  <*> o .:? "coordinates"
-  parseJSON _ = mzero
-
-data EventType = Favorite | Unfavorite
-               | ListCreated | ListUpdated | ListMemberAdded
-               | UserUpdate | Block | Unblock | Follow
-               deriving (Show, Eq)
-
-data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value
-                 deriving (Show, Eq)
-
-instance FromJSON EventTarget where
-  parseJSON v@(Object o) = checkError o >>
-    ETUser <$> parseJSON v <|>
-    ETStatus <$> parseJSON v <|>
-    ETList <$> parseJSON v <|>
-    return (ETUnknown v)
-  parseJSON _ = mzero
-
-data Event =
-  Event
-  { evCreatedAt       :: DateString
-  , evTargetObject    :: Maybe EventTarget
-  , evEvent           :: Text
-  , evTarget          :: EventTarget
-  , evSource          :: EventTarget
-  } deriving (Show, Eq)
-
-instance FromJSON Event where
-  parseJSON (Object o) = checkError o >>
-    Event <$> o .:  "created_at"
-          <*> o .:? "target_object"
-          <*> o .:  "event"
-          <*> o .:  "target"
-          <*> o .:  "source"
-  parseJSON _ = mzero
-
-data Delete =
-  Delete
-  { delId  :: StatusId
-  , delUserId :: UserId
-  } deriving (Show, Eq)
-
-instance FromJSON Delete where
-  parseJSON (Object o) = checkError o >> do
-    s <- o .: "delete" >>= (.: "status")
-    Delete <$> s .: "id"
-           <*> s .: "user_id"
-  parseJSON _ = mzero
-
-data User =
-  User
-  { userId              :: UserId
-  , userName            :: UserName
-  , userScreenName      :: Text
-  , userDescription     :: Maybe Text
-  , userLocation        :: Maybe Text
-  , userProfileImageURL :: Maybe URIString
-  , userURL             :: Maybe URIString
-  , userProtected       :: Maybe Bool
-  , userFollowers       :: Maybe Int
-  , userFriends         :: Maybe Int
-  , userTweets          :: Maybe Int
-  , userLangCode        :: Maybe LanguageCode
-  , userCreatedAt       :: Maybe DateString
-  } deriving (Show, Eq)
-
-instance FromJSON User where
-  parseJSON (Object o) = checkError o >>
-    User <$> o .:  "id"
-         <*> o .:  "name"
-         <*> o .:  "screen_name"
-         <*> o .:? "description"
-         <*> o .:? "location"
-         <*> o .:? "profile_image_url"
-         <*> o .:? "url"
-         <*> o .:? "protected"
-         <*> o .:? "followers_count"
-         <*> o .:? "friends_count"
-         <*> o .:? "statuses_count"
-         <*> o .:? "lang"
-         <*> o .:? "created_at"
-  parseJSON _ = mzero
-
-data List =
-  List
-  { listId :: Int
-  , listName :: Text
-  , listFullName :: Text
-  , listMemberCount :: Int
-  , listSubscriberCount :: Int
-  , listMode :: Text
-  , listUser :: User
-  } deriving (Show, Eq)
-
-instance FromJSON List where
-  parseJSON (Object o) = checkError o >>
-    List <$> o .:  "id"
-         <*> o .:  "name"
-         <*> o .:  "full_name"
-         <*> o .:  "member_count"
-         <*> o .:  "subscriber_count"
-         <*> o .:  "mode"
-         <*> o .:  "user"
-  parseJSON _ = mzero
-
-data HashTagEntity =
-  HashTagEntity
-  { hashTagText :: Text -- ^ The Hashtag text
-  } deriving (Show, Eq)
-
-instance FromJSON HashTagEntity where
-  parseJSON (Object o) =
-    HashTagEntity <$> o .: "text"
-  parseJSON _ = mzero
-
-data UserEntity =
-  UserEntity
-  { userEntityUserId              :: UserId
-  , userEntityUserName            :: UserName
-  , userEntityUserScreenName      :: Text
-  } deriving (Show, Eq)
-
-instance FromJSON UserEntity where
-  parseJSON (Object o) =
-    UserEntity <$> o .:  "id"
-               <*> o .:  "name"
-               <*> o .:  "screen_name"
-  parseJSON _ = mzero
-
-data URLEntity =
-  URLEntity
-  { ueURL      :: URIString -- ^ The URL that was extracted
-  , ueExpanded :: URIString -- ^ The fully resolved URL (only for t.co links)
-  , ueDisplay  :: Text    -- ^ Not a URL but a string to display instead of the URL (only for t.co links)
-  } deriving (Show, Eq)
-
-instance FromJSON URLEntity where
-  parseJSON (Object o) =
-    URLEntity <$> o .:  "url"
-              <*> o .:  "expanded_url"
-              <*> o .:  "display_url"
-  parseJSON _ = mzero
-
-data MediaEntity =
-  MediaEntity
-  { meType :: Text
-  , meId :: StatusId
-  , meSizes :: HashMap Text MediaSize
-  , meMediaURL :: URIString
-  , meMediaURLHttps :: URIString
-  , meURL :: URLEntity
-  } deriving (Show, Eq)
-
-instance FromJSON MediaEntity where
-  parseJSON v@(Object o) =
-    MediaEntity <$> o .: "type"
-                <*> o .: "id"
-                <*> o .: "sizes"
-                <*> o .: "media_url"
-                <*> o .: "media_url_https"
-                <*> parseJSON v
-  parseJSON _ = mzero
-
-data MediaSize =
-  MediaSize
-  { msWidth :: Int
-  , msHeight :: Int
-  , msResize :: Text
-  } deriving (Show, Eq)
-
-instance FromJSON MediaSize where
-  parseJSON (Object o) =
-    MediaSize <$> o .: "w"
-              <*> o .: "h"
-              <*> o .: "resize"
-  parseJSON _ = mzero
-
-data Coordinates =
-  Coordinates
-  { coordinates :: [Double]
-  , coordinatesType :: Text
-  } deriving (Show, Eq)
-
-instance FromJSON Coordinates where
-  parseJSON (Object o) =
-    Coordinates <$> o .: "coordinates"
-                <*> o .: "type"
-  parseJSON _ = mzero
-
-data Place =
-  Place
-  { placeAttributes   :: HashMap Text Text
-  , placeBoundingBox  :: BoundingBox
-  , placeCountry      :: Text
-  , placeCountryCode  :: Text
-  , placeFullName     :: Text
-  , placeId           :: Text
-  , placeName         :: Text
-  , placeType         :: Text
-  , placeUrl          :: Text
-  } deriving (Show, Eq)
-
-instance FromJSON Place where
-  parseJSON (Object o) =
-    Place <$> o .: "attributes"
-          <*> o .: "bounding_box"
-          <*> o .: "country"
-          <*> o .: "country_code"
-          <*> o .: "full_name"
-          <*> o .: "id"
-          <*> o .: "name"
-          <*> o .: "place_type"
-          <*> o .: "url"
-  parseJSON _ = mzero
-
-data BoundingBox =
-  BoundingBox
-  { boundingBoxCoordinates  :: [[[Double]]]
-  , boundingBoxType         :: Text
-  } deriving (Show, Eq)
-
-instance FromJSON BoundingBox where
-  parseJSON (Object o) =
-    BoundingBox <$> o .: "coordinates"
-                <*> o .: "type"
-  parseJSON _ = mzero
-
--- | Entity handling.
-data Entities =
-  Entities
-  { enHashTags     :: [Entity HashTagEntity]
-  , enUserMentions :: [Entity UserEntity]
-  , enURLs         :: [Entity URLEntity]
-  , enMedia        :: [Entity MediaEntity]
-  } deriving (Show, Eq)
-
-instance FromJSON Entities where
-  parseJSON (Object o) =
-    Entities <$> o .:? "hashtags" .!= []
-             <*> o .:? "user_mentions" .!= []
-             <*> o .:? "urls" .!= []
-             <*> o .:? "media" .!= []
-  parseJSON _ = mzero
-
--- | The character positions the Entity was extracted from
---
---   This is experimental implementation.
---   This may be replaced by more definite types.
-type EntityIndices = [Int]
-
-data Entity a =
-  Entity
-  { entityBody    :: a             -- ^ The detail information of the specific entity types (HashTag, URL, User)
-  , entityIndices :: EntityIndices -- ^ The character positions the Entity was extracted from
-  } deriving (Show, Eq)
-
-instance FromJSON a => FromJSON (Entity a) where
-  parseJSON v@(Object o) =
-    Entity <$> parseJSON v
-           <*> o .: "indices"
-  parseJSON _ = mzero
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Web.Twitter.Types (
+    UserId,
+    Friends,
+    URIString,
+    UserName,
+    StatusId,
+    LanguageCode,
+    StreamingAPI (..),
+    Status (..),
+    SearchResult (..),
+    SearchStatus (..),
+    SearchMetadata (..),
+    RetweetedStatus (..),
+    DirectMessage (..),
+    EventTarget (..),
+    Event (..),
+    Delete (..),
+    User (..),
+    List (..),
+    Entities (..),
+    EntityIndices,
+    ExtendedEntities (..),
+    Variant (..),
+    VideoInfo (..),
+    ExtendedEntity (..),
+    Entity (..),
+    HashTagEntity (..),
+    UserEntity (..),
+    URLEntity (..),
+    MediaEntity (..),
+    MediaSize (..),
+    Coordinates (..),
+    Place (..),
+    BoundingBox (..),
+    Contributor (..),
+    UploadedMedia (..),
+    ImageSizeType (..),
+    DisplayTextRange (..),
+    checkError,
+    twitterTimeFormat,
+) where
+
+import Control.Applicative
+import Control.Monad
+import Data.Aeson
+import Data.Aeson.Types (Parser)
+import Data.Data
+import Data.HashMap.Strict (HashMap)
+
+#if MIN_VERSION_aeson(2, 0, 0)
+import qualified Data.Aeson.KeyMap as KeyMap
+#else
+import qualified Data.HashMap.Strict as KeyMap
+#endif
+import Data.Int
+import Data.Ratio
+import Data.Text (Text, pack, unpack)
+import Data.Text.Read (decimal)
+import Data.Time
+import Data.Time.Clock.POSIX
+import GHC.Generics
+
+newtype TwitterTime = TwitterTime {fromTwitterTime :: UTCTime}
+
+type UserId = Integer
+type Friends = [UserId]
+type URIString = Text
+type UserName = Text
+type StatusId = Integer
+type LanguageCode = String
+
+data StreamingAPI
+    = SStatus Status
+    | SRetweetedStatus RetweetedStatus
+    | SEvent Event
+    | SDelete Delete
+    | -- | SScrubGeo ScrubGeo
+      SFriends Friends
+    | SDirectMessage DirectMessage
+    | SUnknown Value
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+checkError :: Object -> Parser ()
+checkError o = do
+    err <- o .:? "error"
+    case err of
+        Just msg -> fail msg
+        Nothing -> return ()
+
+twitterTimeFormat :: String
+twitterTimeFormat = "%a %b %d %T %z %Y"
+
+instance FromJSON TwitterTime where
+    parseJSON = withText "TwitterTime" $ \t ->
+        case parseTimeM True defaultTimeLocale twitterTimeFormat (unpack t) of
+            Just d -> pure $ TwitterTime d
+            Nothing -> fail $ "Could not parse twitter time. Text was: " ++ unpack t
+
+instance ToJSON TwitterTime where
+    toJSON t = String $ pack $ formatTime defaultTimeLocale twitterTimeFormat $ fromTwitterTime t
+
+instance FromJSON StreamingAPI where
+    parseJSON v@(Object o) =
+        SRetweetedStatus <$> js
+            <|> SStatus <$> js
+            <|> SEvent <$> js
+            <|> SDelete <$> js
+            <|> SFriends <$> (o .: "friends")
+            <|> SDirectMessage <$> (o .: "direct_message")
+            <|> return (SUnknown v)
+      where
+        js :: FromJSON a => Parser a
+        js = parseJSON v
+    parseJSON v = fail $ "couldn't parse StreamingAPI from: " ++ show v
+
+instance ToJSON StreamingAPI where
+    toJSON (SStatus s) = toJSON s
+    toJSON (SRetweetedStatus s) = toJSON s
+    toJSON (SEvent e) = toJSON e
+    toJSON (SDelete d) = toJSON d
+    toJSON (SFriends f) = toJSON f
+    toJSON (SDirectMessage m) = toJSON m
+    toJSON (SUnknown v) = v
+
+-- | This type represents a Twitter tweet structure.
+-- See <https://dev.twitter.com/docs/platform-objects/tweets>.
+data Status = Status
+    { statusContributors :: Maybe [Contributor]
+    , statusCoordinates :: Maybe Coordinates
+    , statusCreatedAt :: UTCTime
+    , statusCurrentUserRetweet :: Maybe StatusId
+    , statusEntities :: Maybe Entities
+    , statusExtendedEntities :: Maybe ExtendedEntities
+    , statusFavoriteCount :: Integer
+    , statusFavorited :: Maybe Bool
+    , statusFilterLevel :: Maybe Text
+    , statusId :: StatusId
+    , statusInReplyToScreenName :: Maybe Text
+    , statusInReplyToStatusId :: Maybe StatusId
+    , statusInReplyToUserId :: Maybe UserId
+    , statusLang :: Maybe LanguageCode
+    , statusPlace :: Maybe Place
+    , statusPossiblySensitive :: Maybe Bool
+    , statusScopes :: Maybe Object
+    , statusQuotedStatusId :: Maybe StatusId
+    , statusQuotedStatus :: Maybe Status
+    , statusRetweetCount :: Integer
+    , statusRetweeted :: Maybe Bool
+    , statusRetweetedStatus :: Maybe Status
+    , statusSource :: Text
+    , statusText :: Text
+    , statusTruncated :: Bool
+    , statusUser :: User
+    , statusWithheldCopyright :: Maybe Bool
+    , statusWithheldInCountries :: Maybe [Text]
+    , statusWithheldScope :: Maybe Text
+    , statusDisplayTextRange :: Maybe DisplayTextRange
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Status where
+    parseJSON (Object o) =
+        checkError o
+            >> Status <$> o .:? "contributors" .!= Nothing
+            <*> o .:? "coordinates" .!= Nothing
+            <*> (o .: "created_at" >>= return . fromTwitterTime)
+            <*> ((o .: "current_user_retweet" >>= (.: "id")) <|> return Nothing)
+            <*> o .:? "entities"
+            <*> o .:? "extended_entities"
+            <*> o .:? "favorite_count" .!= 0
+            <*> o .:? "favorited"
+            <*> o .:? "filter_level"
+            <*> o .: "id"
+            <*> o .:? "in_reply_to_screen_name" .!= Nothing
+            <*> o .:? "in_reply_to_status_id" .!= Nothing
+            <*> o .:? "in_reply_to_user_id" .!= Nothing
+            <*> o .:? "lang"
+            <*> o .:? "place" .!= Nothing
+            <*> o .:? "possibly_sensitive"
+            <*> o .:? "scopes"
+            <*> o .:? "quoted_status_id"
+            <*> o .:? "quoted_status"
+            <*> o .:? "retweet_count" .!= 0
+            <*> o .:? "retweeted"
+            <*> o .:? "retweeted_status"
+            <*> o .: "source"
+            <*> (o .: "full_text" <|> o .: "text")
+            <*> o .: "truncated"
+            <*> o .: "user"
+            <*> o .:? "withheld_copyright"
+            <*> o .:? "withheld_in_countries"
+            <*> o .:? "withheld_scope"
+            <*> o .:? "display_text_range"
+    parseJSON v = fail $ "couldn't parse status from: " ++ show v
+
+instance ToJSON Status where
+    toJSON Status {..} =
+        object
+            [ "contributors" .= statusContributors
+            , "coordinates" .= statusCoordinates
+            , "created_at" .= TwitterTime statusCreatedAt
+            , "current_user_retweet"
+                .= object
+                    [ "id" .= statusCurrentUserRetweet
+                    , "id_str" .= show statusCurrentUserRetweet
+                    ]
+            , "entities" .= statusEntities
+            , "extended_entities" .= statusExtendedEntities
+            , "favorite_count" .= statusFavoriteCount
+            , "favorited" .= statusFavorited
+            , "filter_level" .= statusFilterLevel
+            , "id" .= statusId
+            , "in_reply_to_screen_name" .= statusInReplyToScreenName
+            , "in_reply_to_status_id" .= statusInReplyToStatusId
+            , "in_reply_to_user_id" .= statusInReplyToUserId
+            , "lang" .= statusLang
+            , "place" .= statusPlace
+            , "possibly_sensitive" .= statusPossiblySensitive
+            , "scopes" .= statusScopes
+            , "quoted_status_id" .= statusQuotedStatusId
+            , "quoted_status" .= statusQuotedStatus
+            , "retweet_count" .= statusRetweetCount
+            , "retweeted" .= statusRetweeted
+            , "retweeted_status" .= statusRetweetedStatus
+            , "source" .= statusSource
+            , "text" .= statusText
+            , "truncated" .= statusTruncated
+            , "user" .= statusUser
+            , "withheld_copyright" .= statusWithheldCopyright
+            , "withheld_in_countries" .= statusWithheldInCountries
+            , "withheld_scope" .= statusWithheldScope
+            , "display_text_range" .= statusDisplayTextRange
+            ]
+
+data SearchResult body = SearchResult
+    { searchResultStatuses :: body
+    , searchResultSearchMetadata :: SearchMetadata
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance
+    FromJSON body =>
+    FromJSON (SearchResult body)
+    where
+    parseJSON (Object o) =
+        checkError o
+            >> SearchResult <$> o .: "statuses"
+            <*> o .: "search_metadata"
+    parseJSON v = fail $ "couldn't parse search result from: " ++ show v
+
+instance
+    ToJSON body =>
+    ToJSON (SearchResult body)
+    where
+    toJSON SearchResult {..} =
+        object
+            [ "statuses" .= searchResultStatuses
+            , "search_metadata" .= searchResultSearchMetadata
+            ]
+
+data SearchStatus = SearchStatus
+    { searchStatusCreatedAt :: UTCTime
+    , searchStatusId :: StatusId
+    , searchStatusText :: Text
+    , searchStatusSource :: Text
+    , searchStatusUser :: User
+    , searchStatusCoordinates :: Maybe Coordinates
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON SearchStatus where
+    parseJSON (Object o) =
+        checkError o
+            >> SearchStatus <$> (o .: "created_at" >>= return . fromTwitterTime)
+            <*> o .: "id"
+            <*> o .: "text"
+            <*> o .: "source"
+            <*> o .: "user"
+            <*> o .:? "coordinates"
+    parseJSON v = fail $ "couldn't parse status search result from: " ++ show v
+
+instance ToJSON SearchStatus where
+    toJSON SearchStatus {..} =
+        object
+            [ "created_at" .= TwitterTime searchStatusCreatedAt
+            , "id" .= searchStatusId
+            , "text" .= searchStatusText
+            , "source" .= searchStatusSource
+            , "user" .= searchStatusUser
+            , "coordinates" .= searchStatusCoordinates
+            ]
+
+data SearchMetadata = SearchMetadata
+    { searchMetadataMaxId :: StatusId
+    , searchMetadataSinceId :: StatusId
+    , searchMetadataRefreshURL :: URIString
+    , searchMetadataNextResults :: Maybe URIString
+    , searchMetadataCount :: Int
+    , searchMetadataCompletedIn :: Maybe Float
+    , searchMetadataSinceIdStr :: String
+    , searchMetadataQuery :: String
+    , searchMetadataMaxIdStr :: String
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON SearchMetadata where
+    parseJSON (Object o) =
+        checkError o
+            >> SearchMetadata <$> o .: "max_id"
+            <*> o .: "since_id"
+            <*> o .: "refresh_url"
+            <*> o .:? "next_results"
+            <*> o .: "count"
+            <*> o .:? "completed_in"
+            <*> o .: "since_id_str"
+            <*> o .: "query"
+            <*> o .: "max_id_str"
+    parseJSON v = fail $ "couldn't parse search metadata from: " ++ show v
+
+instance ToJSON SearchMetadata where
+    toJSON SearchMetadata {..} =
+        object
+            [ "max_id" .= searchMetadataMaxId
+            , "since_id" .= searchMetadataSinceId
+            , "refresh_url" .= searchMetadataRefreshURL
+            , "next_results" .= searchMetadataNextResults
+            , "count" .= searchMetadataCount
+            , "completed_in" .= searchMetadataCompletedIn
+            , "since_id_str" .= searchMetadataSinceIdStr
+            , "query" .= searchMetadataQuery
+            , "max_id_str" .= searchMetadataMaxIdStr
+            ]
+
+data RetweetedStatus = RetweetedStatus
+    { rsCreatedAt :: UTCTime
+    , rsId :: StatusId
+    , rsText :: Text
+    , rsSource :: Text
+    , rsTruncated :: Bool
+    , rsEntities :: Maybe Entities
+    , rsUser :: User
+    , rsRetweetedStatus :: Status
+    , rsCoordinates :: Maybe Coordinates
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON RetweetedStatus where
+    parseJSON (Object o) =
+        checkError o
+            >> RetweetedStatus <$> (o .: "created_at" >>= return . fromTwitterTime)
+            <*> o .: "id"
+            <*> o .: "text"
+            <*> o .: "source"
+            <*> o .: "truncated"
+            <*> o .:? "entities"
+            <*> o .: "user"
+            <*> o .: "retweeted_status"
+            <*> o .:? "coordinates"
+    parseJSON v = fail $ "couldn't parse retweeted status from: " ++ show v
+
+instance ToJSON RetweetedStatus where
+    toJSON RetweetedStatus {..} =
+        object
+            [ "created_at" .= TwitterTime rsCreatedAt
+            , "id" .= rsId
+            , "text" .= rsText
+            , "source" .= rsSource
+            , "truncated" .= rsTruncated
+            , "entities" .= rsEntities
+            , "user" .= rsUser
+            , "retweeted_status" .= rsRetweetedStatus
+            , "coordinates" .= rsCoordinates
+            ]
+
+type EventId = Integer
+
+data DirectMessage = DirectMessage
+    { dmId :: EventId
+    , dmCreatedTimestamp :: UTCTime
+    , dmTargetRecipientId :: UserId
+    , dmSenderId :: UserId
+    , dmText :: Text
+    , dmEntities :: Entities
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+parseIntegral :: Integral a => Text -> Parser a
+parseIntegral v = either (\_ -> fail $ "couldn't parse stringized int: " ++ show v) (return . fst) $ decimal v
+
+epochMsToUTCTime :: Int64 -> UTCTime
+epochMsToUTCTime = posixSecondsToUTCTime . fromRational . (% 1000) . fromIntegral
+
+parseUnixTimeString :: Text -> Parser UTCTime
+parseUnixTimeString = fmap epochMsToUTCTime <$> parseIntegral
+
+unixTimeToEpochInt :: UTCTime -> Int
+unixTimeToEpochInt = floor . (* 1000) . utcTimeToPOSIXSeconds
+
+instance FromJSON DirectMessage where
+    parseJSON (Object o) = do
+        _ <- checkError o
+        messageCreate <- o .: "message_create"
+        messageData <- messageCreate .: "message_data"
+
+        DirectMessage
+            <$> (o .: "id" >>= parseIntegral)
+            <*> (o .: "created_timestamp" >>= parseUnixTimeString)
+            <*> (messageCreate .: "target" >>= (.: "recipient_id") >>= parseIntegral)
+            <*> (messageCreate .: "sender_id" >>= parseIntegral)
+            <*> messageData .: "text"
+            <*> messageData .: "entities"
+    parseJSON v = fail $ "couldn't parse direct message create event from: " ++ show v
+
+instance ToJSON DirectMessage where
+    toJSON DirectMessage {..} =
+        object
+            [ "id" .= show dmId
+            , "created_timestamp" .= show (unixTimeToEpochInt dmCreatedTimestamp)
+            , "message_create"
+                .= object
+                    [ "message_data" .= object ["text" .= dmText, "entities" .= dmEntities]
+                    , "target" .= object ["recipient_id" .= show dmTargetRecipientId]
+                    , "sender_id" .= show dmSenderId
+                    ]
+            ]
+
+data EventType
+    = Favorite
+    | Unfavorite
+    | ListCreated
+    | ListUpdated
+    | ListMemberAdded
+    | UserUpdate
+    | Block
+    | Unblock
+    | Follow
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON EventTarget where
+    parseJSON v@(Object o) =
+        checkError o
+            >> ETUser <$> parseJSON v
+            <|> ETStatus <$> parseJSON v
+            <|> ETList <$> parseJSON v
+            <|> return (ETUnknown v)
+    parseJSON v = fail $ "couldn't parse event target from: " ++ show v
+
+instance ToJSON EventTarget where
+    toJSON (ETUser u) = toJSON u
+    toJSON (ETStatus s) = toJSON s
+    toJSON (ETList l) = toJSON l
+    toJSON (ETUnknown v) = v
+
+data Event = Event
+    { evCreatedAt :: UTCTime
+    , evTargetObject :: Maybe EventTarget
+    , evEvent :: Text
+    , evTarget :: EventTarget
+    , evSource :: EventTarget
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Event where
+    parseJSON (Object o) =
+        checkError o
+            >> Event <$> (o .: "created_at" >>= return . fromTwitterTime)
+            <*> o .:? "target_object"
+            <*> o .: "event"
+            <*> o .: "target"
+            <*> o .: "source"
+    parseJSON v = fail $ "couldn't parse event from: " ++ show v
+
+instance ToJSON Event where
+    toJSON Event {..} =
+        object
+            [ "created_at" .= TwitterTime evCreatedAt
+            , "target_object" .= evTargetObject
+            , "event" .= evEvent
+            , "target" .= evTarget
+            , "source" .= evSource
+            ]
+
+data Delete = Delete
+    { delId :: StatusId
+    , delUserId :: UserId
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Delete where
+    parseJSON (Object o) =
+        checkError o >> do
+            s <- o .: "delete" >>= (.: "status")
+            Delete <$> s .: "id"
+                <*> s .: "user_id"
+    parseJSON v = fail $ "couldn't parse delete from: " ++ show v
+
+instance ToJSON Delete where
+    toJSON Delete {..} =
+        object
+            [ "delete"
+                .= object
+                    [ "status"
+                        .= object
+                            [ "id" .= delId
+                            , "user_id" .= delUserId
+                            ]
+                    ]
+            ]
+
+-- | This type represents the Twitter user.
+-- See <https://dev.twitter.com/docs/platform-objects/users>.
+data User = User
+    { userContributorsEnabled :: Bool
+    , userCreatedAt :: UTCTime
+    , userDefaultProfile :: Bool
+    , userDefaultProfileImage :: Bool
+    , userDescription :: Maybe Text
+    , userEmail :: Maybe Text
+    , userFavoritesCount :: Int
+    , userFollowRequestSent :: Maybe Bool
+    , userFollowing :: Maybe Bool
+    , userFollowersCount :: Int
+    , userFriendsCount :: Int
+    , userGeoEnabled :: Bool
+    , userId :: UserId
+    , userIsTranslator :: Bool
+    , userLang :: Maybe LanguageCode
+    , userListedCount :: Int
+    , userLocation :: Maybe Text
+    , userName :: Text
+    , userNotifications :: Maybe Bool
+    , userProfileBackgroundColor :: Maybe Text
+    , userProfileBackgroundImageURL :: Maybe URIString
+    , userProfileBackgroundImageURLHttps :: Maybe URIString
+    , userProfileBackgroundTile :: Maybe Bool
+    , userProfileBannerURL :: Maybe URIString
+    , userProfileImageURL :: Maybe URIString
+    , userProfileImageURLHttps :: Maybe URIString
+    , userProfileLinkColor :: Text
+    , userProfileSidebarBorderColor :: Text
+    , userProfileSidebarFillColor :: Text
+    , userProfileTextColor :: Text
+    , userProfileUseBackgroundImage :: Bool
+    , userProtected :: Bool
+    , userScreenName :: Text
+    , userShowAllInlineMedia :: Maybe Bool
+    , userStatusesCount :: Int
+    , userTimeZone :: Maybe Text
+    , userURL :: Maybe URIString
+    , userUtcOffset :: Maybe Int
+    , userVerified :: Bool
+    , userWithheldInCountries :: Maybe [Text]
+    , userWithheldScope :: Maybe Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON User where
+    parseJSON (Object o) =
+        checkError o
+            >> User <$> o .: "contributors_enabled"
+            <*> (o .: "created_at" >>= return . fromTwitterTime)
+            <*> o .: "default_profile"
+            <*> o .: "default_profile_image"
+            <*> o .:? "description"
+            <*> fmap join (o .:? "email") -- The field can be a null value
+            <*> o .: "favourites_count"
+            <*> o .:? "follow_request_sent" .!= Nothing
+            <*> o .:? "following" .!= Nothing
+            <*> o .: "followers_count"
+            <*> o .: "friends_count"
+            <*> o .: "geo_enabled"
+            <*> o .: "id"
+            <*> o .: "is_translator"
+            <*> o .: "lang"
+            <*> o .: "listed_count"
+            <*> o .:? "location"
+            <*> o .: "name"
+            <*> o .:? "notifications" .!= Nothing
+            <*> o .:? "profile_background_color"
+            <*> o .:? "profile_background_image_url"
+            <*> o .:? "profile_background_image_url_https"
+            <*> o .:? "profile_background_tile"
+            <*> o .:? "profile_banner_url"
+            <*> o .:? "profile_image_url"
+            <*> o .:? "profile_image_url_https"
+            <*> o .: "profile_link_color"
+            <*> o .: "profile_sidebar_border_color"
+            <*> o .: "profile_sidebar_fill_color"
+            <*> o .: "profile_text_color"
+            <*> o .: "profile_use_background_image"
+            <*> o .: "protected"
+            <*> o .: "screen_name"
+            <*> o .:? "show_all_inline_media"
+            <*> o .: "statuses_count"
+            <*> o .:? "time_zone"
+            <*> o .:? "url" .!= Nothing
+            <*> o .:? "utc_offset"
+            <*> o .: "verified"
+            <*> o .:? "withheld_in_countries"
+            <*> o .:? "withheld_scope"
+    parseJSON v = fail $ "couldn't parse user from: " ++ show v
+
+instance ToJSON User where
+    toJSON User {..} =
+        object
+            [ "contributors_enabled" .= userContributorsEnabled
+            , "created_at" .= TwitterTime userCreatedAt
+            , "default_profile" .= userDefaultProfile
+            , "default_profile_image" .= userDefaultProfileImage
+            , "description" .= userDescription
+            , "email" .= userEmail
+            , "favourites_count" .= userFavoritesCount
+            , "follow_request_sent" .= userFollowRequestSent
+            , "following" .= userFollowing
+            , "followers_count" .= userFollowersCount
+            , "friends_count" .= userFriendsCount
+            , "geo_enabled" .= userGeoEnabled
+            , "id" .= userId
+            , "is_translator" .= userIsTranslator
+            , "lang" .= userLang
+            , "listed_count" .= userListedCount
+            , "location" .= userLocation
+            , "name" .= userName
+            , "notifications" .= userNotifications
+            , "profile_background_color" .= userProfileBackgroundColor
+            , "profile_background_image_url" .= userProfileBackgroundImageURL
+            , "profile_background_image_url_https" .= userProfileBackgroundImageURLHttps
+            , "profile_background_tile" .= userProfileBackgroundTile
+            , "profile_banner_url" .= userProfileBannerURL
+            , "profile_image_url" .= userProfileImageURL
+            , "profile_image_url_https" .= userProfileImageURLHttps
+            , "profile_link_color" .= userProfileLinkColor
+            , "profile_sidebar_border_color" .= userProfileSidebarBorderColor
+            , "profile_sidebar_fill_color" .= userProfileSidebarFillColor
+            , "profile_text_color" .= userProfileTextColor
+            , "profile_use_background_image" .= userProfileUseBackgroundImage
+            , "protected" .= userProtected
+            , "screen_name" .= userScreenName
+            , "show_all_inline_media" .= userShowAllInlineMedia
+            , "statuses_count" .= userStatusesCount
+            , "time_zone" .= userTimeZone
+            , "url" .= userURL
+            , "utc_offset" .= userUtcOffset
+            , "verified" .= userVerified
+            , "withheld_in_countries" .= userWithheldInCountries
+            , "withheld_scope" .= userWithheldScope
+            ]
+
+data List = List
+    { listId :: Int
+    , listName :: Text
+    , listFullName :: Text
+    , listMemberCount :: Int
+    , listSubscriberCount :: Int
+    , listMode :: Text
+    , listUser :: User
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON List where
+    parseJSON (Object o) =
+        checkError o
+            >> List <$> o .: "id"
+            <*> o .: "name"
+            <*> o .: "full_name"
+            <*> o .: "member_count"
+            <*> o .: "subscriber_count"
+            <*> o .: "mode"
+            <*> o .: "user"
+    parseJSON v = fail $ "couldn't parse List from: " ++ show v
+
+instance ToJSON List where
+    toJSON List {..} =
+        object
+            [ "id" .= listId
+            , "name" .= listName
+            , "full_name" .= listFullName
+            , "member_count" .= listMemberCount
+            , "subscriber_count" .= listSubscriberCount
+            , "mode" .= listMode
+            , "user" .= listUser
+            ]
+
+-- | Hashtag entity.
+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-hashtags>.
+data HashTagEntity = HashTagEntity
+    { -- | The Hashtag text
+      hashTagText :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON HashTagEntity where
+    parseJSON (Object o) =
+        HashTagEntity <$> o .: "text"
+    parseJSON v = fail $ "couldn't parse hashtag entity from: " ++ show v
+
+instance ToJSON HashTagEntity where
+    toJSON HashTagEntity {..} = object ["text" .= hashTagText]
+
+-- | User mention entity.
+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-usermention>.
+data UserEntity = UserEntity
+    { userEntityUserId :: UserId
+    , userEntityUserName :: UserName
+    , userEntityUserScreenName :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON UserEntity where
+    parseJSON (Object o) =
+        UserEntity <$> o .: "id"
+            <*> o .: "name"
+            <*> o .: "screen_name"
+    parseJSON v = fail $ "couldn't parse user entity from: " ++ show v
+
+instance ToJSON UserEntity where
+    toJSON UserEntity {..} =
+        object
+            [ "id" .= userEntityUserId
+            , "name" .= userEntityUserName
+            , "screen_name" .= userEntityUserScreenName
+            ]
+
+-- | URL entity.
+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-url>.
+data URLEntity = URLEntity
+    { -- | The URL that was extracted
+      ueURL :: URIString
+    , -- | The fully resolved URL (only for t.co links)
+      ueExpanded :: URIString
+    , -- | Not a URL but a string to display instead of the URL (only for t.co links)
+      ueDisplay :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON URLEntity where
+    parseJSON (Object o) =
+        URLEntity <$> o .: "url"
+            <*> o .: "expanded_url"
+            <*> o .: "display_url"
+    parseJSON v = fail $ "couldn't parse url entity from: " ++ show v
+
+instance ToJSON URLEntity where
+    toJSON URLEntity {..} =
+        object
+            [ "url" .= ueURL
+            , "expanded_url" .= ueExpanded
+            , "display_url" .= ueDisplay
+            ]
+
+data MediaEntity = MediaEntity
+    { meType :: Text
+    , meId :: StatusId
+    , meSizes :: HashMap Text MediaSize
+    , meMediaURL :: URIString
+    , meMediaURLHttps :: URIString
+    , meURL :: URLEntity
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON MediaEntity where
+    parseJSON v@(Object o) =
+        MediaEntity <$> o .: "type"
+            <*> o .: "id"
+            <*> o .: "sizes"
+            <*> o .: "media_url"
+            <*> o .: "media_url_https"
+            <*> parseJSON v
+    parseJSON v = fail $ "couldn't parse media entity from: " ++ show v
+
+instance ToJSON MediaEntity where
+    toJSON MediaEntity {..} =
+        object
+            [ "type" .= meType
+            , "id" .= meId
+            , "sizes" .= meSizes
+            , "media_url" .= meMediaURL
+            , "media_url_https" .= meMediaURLHttps
+            , "url" .= ueURL meURL
+            , "expanded_url" .= ueExpanded meURL
+            , "display_url" .= ueDisplay meURL
+            ]
+
+-- | Size entity.
+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-size>.
+data MediaSize = MediaSize
+    { msWidth :: Int
+    , msHeight :: Int
+    , msResize :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON MediaSize where
+    parseJSON (Object o) =
+        MediaSize <$> o .: "w"
+            <*> o .: "h"
+            <*> o .: "resize"
+    parseJSON v = fail $ "couldn't parse media size from: " ++ show v
+
+instance ToJSON MediaSize where
+    toJSON MediaSize {..} =
+        object
+            [ "w" .= msWidth
+            , "h" .= msHeight
+            , "resize" .= msResize
+            ]
+
+data Coordinates = Coordinates
+    { coordinates :: [Double]
+    , coordinatesType :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Coordinates where
+    parseJSON (Object o) =
+        Coordinates <$> o .: "coordinates"
+            <*> o .: "type"
+    parseJSON v = fail $ "couldn't parse coordinates from: " ++ show v
+
+instance ToJSON Coordinates where
+    toJSON Coordinates {..} =
+        object
+            [ "coordinates" .= coordinates
+            , "type" .= coordinatesType
+            ]
+
+-- | This type represents a place, named locations with corresponding geo coordinates.
+-- See <https://dev.twitter.com/docs/platform-objects/places>.
+data Place = Place
+    { placeAttributes :: HashMap Text Text
+    , placeBoundingBox :: Maybe BoundingBox
+    , placeCountry :: Text
+    , placeCountryCode :: Text
+    , placeFullName :: Text
+    , placeId :: Text
+    , placeName :: Text
+    , placeType :: Text
+    , placeURL :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Place where
+    parseJSON (Object o) =
+        Place <$> o .: "attributes"
+            <*> o .:? "bounding_box"
+            <*> o .: "country"
+            <*> o .: "country_code"
+            <*> o .: "full_name"
+            <*> o .: "id"
+            <*> o .: "name"
+            <*> o .: "place_type"
+            <*> o .: "url"
+    parseJSON v = fail $ "couldn't parse place from: " ++ show v
+
+instance ToJSON Place where
+    toJSON Place {..} =
+        object
+            [ "attributes" .= placeAttributes
+            , "bounding_box" .= placeBoundingBox
+            , "country" .= placeCountry
+            , "country_code" .= placeCountryCode
+            , "full_name" .= placeFullName
+            , "id" .= placeId
+            , "name" .= placeName
+            , "place_type" .= placeType
+            , "url" .= placeURL
+            ]
+
+-- | A bounding box of coordinates which encloses the place.
+-- See <https://dev.twitter.com/docs/platform-objects/places#obj-boundingbox>.
+data BoundingBox = BoundingBox
+    { boundingBoxCoordinates :: [[[Double]]]
+    , boundingBoxType :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON BoundingBox where
+    parseJSON (Object o) =
+        BoundingBox <$> o .: "coordinates"
+            <*> o .: "type"
+    parseJSON v = fail $ "couldn't parse bounding box from: " ++ show v
+
+instance ToJSON BoundingBox where
+    toJSON BoundingBox {..} =
+        object
+            [ "coordinates" .= boundingBoxCoordinates
+            , "type" .= boundingBoxType
+            ]
+
+-- | Entity handling.
+-- See <https://dev.twitter.com/docs/platform-objects/entities>.
+data Entities = Entities
+    { enHashTags :: [Entity HashTagEntity]
+    , enUserMentions :: [Entity UserEntity]
+    , enURLs :: [Entity URLEntity]
+    , enMedia :: [Entity MediaEntity]
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Entities where
+    parseJSON (Object o) =
+        Entities <$> o .:? "hashtags" .!= []
+            <*> o .:? "user_mentions" .!= []
+            <*> o .:? "urls" .!= []
+            <*> o .:? "media" .!= []
+    parseJSON v = fail $ "couldn't parse entities from: " ++ show v
+
+instance ToJSON Entities where
+    toJSON Entities {..} =
+        object
+            [ "hashtags" .= enHashTags
+            , "user_mentions" .= enUserMentions
+            , "urls" .= enURLs
+            , "media" .= enMedia
+            ]
+
+-- | The character positions the Entity was extracted from
+--
+--   This is experimental implementation.
+--   This may be replaced by more definite types.
+type EntityIndices = [Int]
+
+data Entity a = Entity
+    { -- | The detail information of the specific entity types (HashTag, URL, User)
+      entityBody :: a
+    , -- | The character positions the Entity was extracted from
+      entityIndices :: EntityIndices
+    }
+    deriving (Show, Eq, Data, Typeable, Generic, Generic1)
+
+instance FromJSON a => FromJSON (Entity a) where
+    parseJSON v@(Object o) =
+        Entity <$> parseJSON v
+            <*> o .: "indices"
+    parseJSON v = fail $ "couldn't parse entity wrapper from: " ++ show v
+
+instance ToJSON a => ToJSON (Entity a) where
+    toJSON Entity {..} =
+        case toJSON entityBody of
+            (Object o) -> Object $ KeyMap.insert "indices" (toJSON entityIndices) o
+            _ -> error "Entity body must produce an object."
+
+data ExtendedEntities = ExtendedEntities
+    { exeMedia :: [Entity ExtendedEntity]
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON ExtendedEntities where
+    parseJSON (Object o) =
+        ExtendedEntities <$> o .:? "media" .!= []
+    parseJSON v = fail $ "couldn't parse extended entity from: " ++ show v
+
+instance ToJSON ExtendedEntities where
+    toJSON ExtendedEntities {..} = object ["media" .= exeMedia]
+
+-- "video_info": {
+--   "aspect_ratio": [
+--     9,
+--     16
+--   ],
+--   "duration_millis": 10704,
+--   "variants": [
+--     {
+--       "bitrate": 320000,
+--       "content_type": "video/mp4",
+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/180x320/FMei8yCw7yc_Z7e-.mp4"
+--     },
+--     {
+--       "bitrate": 2176000,
+--       "content_type": "video/mp4",
+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/720x1280/octt5pFbISkef8RB.mp4"
+--     },
+--     {
+--       "bitrate": 832000,
+--       "content_type": "video/mp4",
+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/360x640/2OmqK74SQ9jNX8mZ.mp4"
+--     },
+--     {
+--       "content_type": "application/x-mpegURL",
+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/pl/wcJQJ2nxiFU4ZZng.m3u8"
+--     }
+--   ]
+-- }
+
+data Variant = Variant
+    { vBitrate :: Maybe Int
+    , vContentType :: Text
+    , vUrl :: URIString
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Variant where
+    parseJSON (Object o) =
+        Variant <$> o .:? "bitrate"
+            <*> o .: "content_type"
+            <*> o .: "url"
+    parseJSON v = fail $ "couldn't parse variant from:" ++ show v
+
+instance ToJSON Variant where
+    toJSON Variant {..} =
+        object
+            [ "bitrate" .= vBitrate
+            , "content_type" .= vContentType
+            , "url" .= vUrl
+            ]
+
+data VideoInfo = VideoInfo
+    { vsAspectRatio :: [Int]
+    , vsDurationMillis :: Maybe Int
+    , vsVariants :: [Variant]
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON VideoInfo where
+    parseJSON (Object o) =
+        VideoInfo <$> o .: "aspect_ratio" .!= []
+            <*> o .:? "duration_millis"
+            <*> o .: "variants" .!= []
+    parseJSON v = fail $ "couldn't parse video info from:" ++ show v
+
+instance ToJSON VideoInfo where
+    toJSON VideoInfo {..} =
+        object
+            [ "aspect_ratio" .= vsAspectRatio
+            , "duration_millis" .= vsDurationMillis
+            , "variants" .= vsVariants
+            ]
+
+-- Extended entities are like entities, but contain special media features like
+-- video or multiple photos
+data ExtendedEntity = ExtendedEntity
+    { exeID :: StatusId
+    , exeMediaUrl :: URIString
+    , exeMediaUrlHttps :: URIString
+    , exeSizes :: HashMap Text MediaSize
+    , exeType :: Text
+    , exeVideoInfo :: Maybe VideoInfo
+    , exeDurationMillis :: Maybe Double
+    , exeExtAltText :: Maybe String
+    , exeURL :: URLEntity
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON ExtendedEntity where
+    parseJSON v@(Object o) =
+        ExtendedEntity <$> o .: "id"
+            <*> o .: "media_url"
+            <*> o .: "media_url_https"
+            <*> o .: "sizes"
+            <*> o .: "type"
+            <*> o .:? "video_info"
+            <*> o .:? "duration_millis"
+            <*> o .:? "ext_alt_text"
+            <*> parseJSON v
+    parseJSON v = fail $ "couldn't parse extended entity from:" ++ show v
+
+instance ToJSON ExtendedEntity where
+    toJSON ExtendedEntity {..} =
+        object
+            [ "id" .= exeID
+            , "media_url" .= exeMediaUrl
+            , "media_url_https" .= exeMediaUrlHttps
+            , "sizes" .= exeSizes
+            , "type" .= exeType
+            , "video_info" .= exeVideoInfo
+            , "duration_millis" .= exeDurationMillis
+            , "ext_alt_text" .= exeExtAltText
+            , "url" .= ueURL exeURL
+            , "expanded_url" .= ueExpanded exeURL
+            , "display_url" .= ueDisplay exeURL
+            ]
+
+data Contributor = Contributor
+    { contributorId :: UserId
+    , contributorScreenName :: Maybe Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON Contributor where
+    parseJSON (Object o) =
+        Contributor <$> o .: "id"
+            <*> o .:? "screen_name"
+    parseJSON v@(Number _) =
+        Contributor <$> parseJSON v <*> pure Nothing
+    parseJSON v = fail $ "couldn't parse contributor from: " ++ show v
+
+instance ToJSON Contributor where
+    toJSON Contributor {..} =
+        object
+            [ "id" .= contributorId
+            , "screen_name" .= contributorScreenName
+            ]
+
+-- | Image size type. This type is included in the API response of \"\/1.1\/media\/upload.json\".
+data ImageSizeType = ImageSizeType
+    { imageSizeTypeWidth :: Int
+    , imageSizeTypeHeight :: Int
+    , imageSizeTypeType :: Text
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON ImageSizeType where
+    parseJSON (Object o) =
+        ImageSizeType <$> o .: "w"
+            <*> o .: "h"
+            <*> o .: "image_type"
+    parseJSON v = fail $ "unknown value: " ++ show v
+
+instance ToJSON ImageSizeType where
+    toJSON ImageSizeType {..} =
+        object
+            [ "w" .= imageSizeTypeWidth
+            , "h" .= imageSizeTypeHeight
+            , "image_type" .= imageSizeTypeType
+            ]
+
+-- | This type is represents the API response of \"\/1.1\/media\/upload.json\".
+-- See <https://dev.twitter.com/docs/api/multiple-media-extended-entities>.
+data UploadedMedia = UploadedMedia
+    { uploadedMediaId :: Integer
+    , uploadedMediaSize :: Integer
+    , uploadedMediaImage :: ImageSizeType
+    }
+    deriving (Show, Eq, Data, Typeable, Generic)
+
+instance FromJSON UploadedMedia where
+    parseJSON (Object o) =
+        UploadedMedia <$> o .: "media_id"
+            <*> o .: "size"
+            <*> o .: "image"
+    parseJSON v = fail $ "unknown value: " ++ show v
+
+instance ToJSON UploadedMedia where
+    toJSON UploadedMedia {..} =
+        object
+            [ "media_id" .= uploadedMediaId
+            , "size" .= uploadedMediaSize
+            , "image" .= uploadedMediaImage
+            ]
+
+-- | unicode code point indices, identifying the inclusive start and exclusive end of the displayable content of the Tweet.
+data DisplayTextRange = DisplayTextRange
+    { displayTextRangeStart :: Int
+    , -- | exclusive
+      displayTextRangeEnd :: Int
+    }
+    deriving (Show, Eq, Ord, Data, Typeable, Generic)
+
+instance FromJSON DisplayTextRange where
+    parseJSON v = do
+        parseJSON v >>= \case
+            [s, e] -> pure $ DisplayTextRange s e
+            unexpected ->
+                fail $ "parsing DisplayTextRange failed, expected [Int, Int], but got: " ++ show unexpected
+instance ToJSON DisplayTextRange where
+    toJSON (DisplayTextRange s e) = toJSON [s, e]
diff --git a/Web/Twitter/Types/Lens.hs b/Web/Twitter/Types/Lens.hs
deleted file mode 100644
--- a/Web/Twitter/Types/Lens.hs
+++ /dev/null
@@ -1,415 +0,0 @@
-{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, RankNTypes, CPP, FlexibleInstances #-}
-
-module Web.Twitter.Types.Lens
-       ( DateString
-       , UserId
-       , Friends
-       , URIString
-       , UserName
-       , StatusId
-       , LanguageCode
-       , StreamingAPI(..)
-       , Status
-       , SearchResult
-       , SearchStatus
-       , SearchMetadata
-       , RetweetedStatus
-       , DirectMessage
-       , EventTarget(..)
-       , Event
-       , Delete
-       , User
-       , List
-       , Entities
-       , EntityIndices
-       , Entity
-       , HashTagEntity
-       , UserEntity
-       , URLEntity
-       , MediaEntity
-       , MediaSize
-       , Coordinates
-       , Place
-       , BoundingBox
-
-       , statusCreatedAt
-       , statusId
-       , statusText
-       , statusSource
-       , statusTruncated
-       , statusEntities
-       , statusExtendedEntities
-       , statusInReplyTo
-       , statusInReplyToUser
-       , statusFavorite
-       , statusRetweetCount
-       , statusRetweet
-       , statusUser
-       , statusPlace
-       , statusFavoriteCount
-       , statusLang
-       , statusPossiblySensitive
-       , statusCoordinates
-
-       , searchResultStatuses
-       , searchResultSearchMetadata
-
-       , searchStatusCreatedAt
-       , searchStatusId
-       , searchStatusText
-       , searchStatusSource
-       , searchStatusUser
-       , searchStatusCoordinates
-
-       , searchMetadataMaxId
-       , searchMetadataSinceId
-       , searchMetadataRefreshUrl
-       , searchMetadataNextResults
-       , searchMetadataCount
-       , searchMetadataCompletedIn
-       , searchMetadataSinceIdStr
-       , searchMetadataQuery
-       , searchMetadataMaxIdStr
-
-       , rsCreatedAt
-       , rsId
-       , rsText
-       , rsSource
-       , rsTruncated
-       , rsEntities
-       , rsUser
-       , rsRetweetedStatus
-       , rsCoordinates
-
-       , dmCreatedAt
-       , dmSenderScreenName
-       , dmSender
-       , dmText
-       , dmRecipientScreeName
-       , dmId
-       , dmRecipient
-       , dmRecipientId
-       , dmSenderId
-       , dmCoordinates
-
-       , evCreatedAt
-       , evTargetObject
-       , evEvent
-       , evTarget
-       , evSource
-
-       , delId
-       , delUserId
-
-       , userId
-       , userName
-       , userScreenName
-       , userDescription
-       , userLocation
-       , userProfileImageURL
-       , userURL
-       , userProtected
-       , userFollowers
-       , userFriends
-       , userTweets
-       , userLangCode
-       , userCreatedAt
-
-       , listId
-       , listName
-       , listFullName
-       , listMemberCount
-       , listSubscriberCount
-       , listMode
-       , listUser
-
-       , hashTagText
-
-       , userEntityUserId
-       , userEntityUserName
-       , userEntityUserScreenName
-
-       , ueURL
-       , ueExpanded
-       , ueDisplay
-
-       , meType
-       , meId
-       , meSizes
-       , meMediaURL
-       , meMediaURLHttps
-       , meURL
-
-       , msWidth
-       , msHeight
-       , msResize
-
-       , coordinates
-       , coordinatesType
-
-       , placeAttributes
-       , placeBoundingBox
-       , placeCountry
-       , placeCountryCode
-       , placeFullName
-       , placeId
-       , placeName
-       , placeType
-       , placeUrl
-
-       , boundingBoxCoordinates
-       , boundingBoxType
-
-       , enHashTags
-       , enUserMentions
-       , enURLs
-       , enMedia
-
-       , entityBody
-       , entityIndices
-
-       , AsStatus(..)
-       , AsUser(..)
-       )
-       where
-
-import Data.Functor
-import qualified Web.Twitter.Types as TT
-import Web.Twitter.Types
-       ( DateString
-       , UserId
-       , Friends
-       , URIString
-       , UserName
-       , StatusId
-       , LanguageCode
-       , StreamingAPI
-       , Status
-       , SearchResult
-       , SearchStatus
-       , SearchMetadata
-       , RetweetedStatus
-       , DirectMessage
-       , EventTarget
-       , Event
-       , Delete
-       , User
-       , List
-       , Entities
-       , EntityIndices
-       , Entity
-       , HashTagEntity
-       , UserEntity
-       , URLEntity
-       , MediaEntity
-       , MediaSize
-       , Coordinates
-       , Place
-       , BoundingBox
-       )
-import Data.Text (Text)
-import Data.HashMap.Strict (HashMap)
-
-type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
-type SimpleLens s a = Lens s s a a
-
-#define HASHSIGN #
-#define SIMPLE_LENS(name, s, a) \
-name :: SimpleLens (s) (a);\
-name f record = (\newVal -> record { TT.name = newVal }) <$> (f (TT.name record));\
-{-HASHSIGN INLINE name HASHSIGN-}
-
-#define TYPECHANGE_LENS(name, s) \
-name :: Lens ((s) a) ((s) b) (a) (b);\
-name f record = (\newVal -> record { TT.name = newVal }) <$> (f (TT.name record))
-
-SIMPLE_LENS(statusCreatedAt           , Status,  DateString                   )
-SIMPLE_LENS(statusId                  , Status,  StatusId                     )
-SIMPLE_LENS(statusText                , Status,  Text                         )
-SIMPLE_LENS(statusSource              , Status,  Text                         )
-SIMPLE_LENS(statusTruncated           , Status,  Bool                         )
-SIMPLE_LENS(statusEntities            , Status,  Maybe Entities               )
-SIMPLE_LENS(statusExtendedEntities    , Status,  Maybe Entities               )
-SIMPLE_LENS(statusInReplyTo           , Status,  Maybe StatusId               )
-SIMPLE_LENS(statusInReplyToUser       , Status,  Maybe UserId                 )
-SIMPLE_LENS(statusFavorite            , Status,  Maybe Bool                   )
-SIMPLE_LENS(statusRetweetCount        , Status,  Maybe Integer                )
-SIMPLE_LENS(statusRetweet             , Status,  Maybe Status                 )
-SIMPLE_LENS(statusUser                , Status,  User                         )
-SIMPLE_LENS(statusPlace               , Status,  Maybe Place                  )
-SIMPLE_LENS(statusFavoriteCount       , Status,  Integer                      )
-SIMPLE_LENS(statusLang                , Status,  Maybe Text                   )
-SIMPLE_LENS(statusPossiblySensitive   , Status,  Maybe Bool                   )
-SIMPLE_LENS(statusCoordinates         , Status,  Maybe Coordinates            )
-
-TYPECHANGE_LENS(searchResultStatuses  , SearchResult                          )
-SIMPLE_LENS(searchResultSearchMetadata, SearchResult body,  SearchMetadata    )
-
-SIMPLE_LENS(searchStatusCreatedAt     , SearchStatus,  DateString             )
-SIMPLE_LENS(searchStatusId            , SearchStatus,  StatusId               )
-SIMPLE_LENS(searchStatusText          , SearchStatus,  Text                   )
-SIMPLE_LENS(searchStatusSource        , SearchStatus,  Text                   )
-SIMPLE_LENS(searchStatusUser          , SearchStatus,  User                   )
-SIMPLE_LENS(searchStatusCoordinates   , SearchStatus,  Maybe Coordinates      )
-
-SIMPLE_LENS(searchMetadataMaxId       , SearchMetadata,  StatusId             )
-SIMPLE_LENS(searchMetadataSinceId     , SearchMetadata,  StatusId             )
-SIMPLE_LENS(searchMetadataRefreshUrl  , SearchMetadata,  URIString            )
-SIMPLE_LENS(searchMetadataNextResults , SearchMetadata,  Maybe URIString      )
-SIMPLE_LENS(searchMetadataCount       , SearchMetadata,  Int                  )
-SIMPLE_LENS(searchMetadataCompletedIn , SearchMetadata,  Maybe Float          )
-SIMPLE_LENS(searchMetadataSinceIdStr  , SearchMetadata,  String               )
-SIMPLE_LENS(searchMetadataQuery       , SearchMetadata,  String               )
-SIMPLE_LENS(searchMetadataMaxIdStr    , SearchMetadata,  String               )
-
-SIMPLE_LENS(rsCreatedAt               , RetweetedStatus,  DateString          )
-SIMPLE_LENS(rsId                      , RetweetedStatus,  StatusId            )
-SIMPLE_LENS(rsText                    , RetweetedStatus,  Text                )
-SIMPLE_LENS(rsSource                  , RetweetedStatus,  Text                )
-SIMPLE_LENS(rsTruncated               , RetweetedStatus,  Bool                )
-SIMPLE_LENS(rsEntities                , RetweetedStatus,  Maybe Entities      )
-SIMPLE_LENS(rsUser                    , RetweetedStatus,  User                )
-SIMPLE_LENS(rsRetweetedStatus         , RetweetedStatus,  Status              )
-SIMPLE_LENS(rsCoordinates             , RetweetedStatus,  Maybe Coordinates   )
-
-SIMPLE_LENS(dmCreatedAt               , DirectMessage,  DateString            )
-SIMPLE_LENS(dmSenderScreenName        , DirectMessage,  Text                  )
-SIMPLE_LENS(dmSender                  , DirectMessage,  User                  )
-SIMPLE_LENS(dmText                    , DirectMessage,  Text                  )
-SIMPLE_LENS(dmRecipientScreeName      , DirectMessage,  Text                  )
-SIMPLE_LENS(dmId                      , DirectMessage,  StatusId              )
-SIMPLE_LENS(dmRecipient               , DirectMessage,  User                  )
-SIMPLE_LENS(dmRecipientId             , DirectMessage,  UserId                )
-SIMPLE_LENS(dmSenderId                , DirectMessage,  UserId                )
-SIMPLE_LENS(dmCoordinates             , DirectMessage,  Maybe Coordinates     )
-
-SIMPLE_LENS(evCreatedAt               , Event,  DateString                    )
-SIMPLE_LENS(evTargetObject            , Event,  Maybe EventTarget             )
-SIMPLE_LENS(evEvent                   , Event,  Text                          )
-SIMPLE_LENS(evTarget                  , Event,  EventTarget                   )
-SIMPLE_LENS(evSource                  , Event,  EventTarget                   )
-
-SIMPLE_LENS(delId                     , Delete,  StatusId                     )
-SIMPLE_LENS(delUserId                 , Delete,  UserId                       )
-
-SIMPLE_LENS(userId                    , User,  UserId                         )
-SIMPLE_LENS(userName                  , User,  UserName                       )
-SIMPLE_LENS(userScreenName            , User,  Text                           )
-SIMPLE_LENS(userDescription           , User,  Maybe Text                     )
-SIMPLE_LENS(userLocation              , User,  Maybe Text                     )
-SIMPLE_LENS(userProfileImageURL       , User,  Maybe URIString                )
-SIMPLE_LENS(userURL                   , User,  Maybe URIString                )
-SIMPLE_LENS(userProtected             , User,  Maybe Bool                     )
-SIMPLE_LENS(userFollowers             , User,  Maybe Int                      )
-SIMPLE_LENS(userFriends               , User,  Maybe Int                      )
-SIMPLE_LENS(userTweets                , User,  Maybe Int                      )
-SIMPLE_LENS(userLangCode              , User,  Maybe LanguageCode             )
-SIMPLE_LENS(userCreatedAt             , User,  Maybe DateString               )
-
-SIMPLE_LENS(listId                    , List,  Int                            )
-SIMPLE_LENS(listName                  , List,  Text                           )
-SIMPLE_LENS(listFullName              , List,  Text                           )
-SIMPLE_LENS(listMemberCount           , List,  Int                            )
-SIMPLE_LENS(listSubscriberCount       , List,  Int                            )
-SIMPLE_LENS(listMode                  , List,  Text                           )
-SIMPLE_LENS(listUser                  , List,  User                           )
-
-SIMPLE_LENS(hashTagText               , HashTagEntity,  Text                  )
-
-SIMPLE_LENS(userEntityUserId          , UserEntity,  UserId                   )
-SIMPLE_LENS(userEntityUserName        , UserEntity,  UserName                 )
-SIMPLE_LENS(userEntityUserScreenName  , UserEntity,  Text                     )
-
-SIMPLE_LENS(ueURL                     , URLEntity,  URIString                 )
-SIMPLE_LENS(ueExpanded                , URLEntity,  URIString                 )
-SIMPLE_LENS(ueDisplay                 , URLEntity,  Text                      )
-
-SIMPLE_LENS(meType                    , MediaEntity,  Text                    )
-SIMPLE_LENS(meId                      , MediaEntity,  StatusId                )
-SIMPLE_LENS(meSizes                   , MediaEntity,  HashMap Text MediaSize  )
-SIMPLE_LENS(meMediaURL                , MediaEntity,  URIString               )
-SIMPLE_LENS(meMediaURLHttps           , MediaEntity,  URIString               )
-SIMPLE_LENS(meURL                     , MediaEntity,  URLEntity               )
-
-SIMPLE_LENS(msWidth                   , MediaSize,  Int                       )
-SIMPLE_LENS(msHeight                  , MediaSize,  Int                       )
-SIMPLE_LENS(msResize                  , MediaSize,  Text                      )
-
-SIMPLE_LENS(coordinates               , Coordinates,  [Double]                )
-SIMPLE_LENS(coordinatesType           , Coordinates,  Text                    )
-
-SIMPLE_LENS(placeAttributes           , Place,  HashMap Text Text             )
-SIMPLE_LENS(placeBoundingBox          , Place,  BoundingBox                   )
-SIMPLE_LENS(placeCountry              , Place,  Text                          )
-SIMPLE_LENS(placeCountryCode          , Place,  Text                          )
-SIMPLE_LENS(placeFullName             , Place,  Text                          )
-SIMPLE_LENS(placeId                   , Place,  Text                          )
-SIMPLE_LENS(placeName                 , Place,  Text                          )
-SIMPLE_LENS(placeType                 , Place,  Text                          )
-SIMPLE_LENS(placeUrl                  , Place,  Text                          )
-
-SIMPLE_LENS(boundingBoxCoordinates    , BoundingBox,  [[[Double]]]            )
-SIMPLE_LENS(boundingBoxType           , BoundingBox,  Text                    )
-
-SIMPLE_LENS(enHashTags                , Entities,  [Entity HashTagEntity]     )
-SIMPLE_LENS(enUserMentions            , Entities,  [Entity UserEntity]        )
-SIMPLE_LENS(enURLs                    , Entities,  [Entity URLEntity]         )
-SIMPLE_LENS(enMedia                   , Entities,  [Entity MediaEntity]       )
-
-TYPECHANGE_LENS(entityBody            , Entity                                )
-SIMPLE_LENS(entityIndices             , Entity a,  EntityIndices              )
-
-class AsStatus s where
-    status_id :: SimpleLens s StatusId
-    text :: SimpleLens s Text
-    user :: SimpleLens s User
-    created_at :: SimpleLens s DateString
-    geolocation :: SimpleLens s (Maybe Coordinates)
-
-instance AsStatus Status where
-    status_id = statusId
-    text = statusText
-    user = statusUser
-    created_at = statusCreatedAt
-    geolocation = statusCoordinates
-
-instance AsStatus SearchStatus where
-    status_id = searchStatusId
-    text = searchStatusText
-    user = searchStatusUser
-    created_at = searchStatusCreatedAt
-    geolocation = searchStatusCoordinates
-
-instance AsStatus RetweetedStatus where
-    status_id = rsId
-    text = rsText
-    user = rsUser
-    created_at = rsCreatedAt
-    geolocation = rsCoordinates
-
-instance AsStatus DirectMessage where
-    status_id = dmId
-    text = dmText
-    user = dmSender
-    created_at = dmCreatedAt
-    geolocation = dmCoordinates
-
-class AsUser u where
-    user_id :: SimpleLens u UserId
-    name :: SimpleLens u UserName
-    screen_name :: SimpleLens u Text
-
-instance AsUser User where
-    user_id = userId
-    name = userName
-    screen_name = userScreenName
-
-instance AsUser UserEntity where
-    user_id = userEntityUserId
-    name = userEntityUserName
-    screen_name = userEntityUserScreenName
-
-instance AsUser (Entity UserEntity) where
-    user_id = entityBody.userEntityUserId
-    name = entityBody.userEntityUserName
-    screen_name = entityBody.userEntityUserScreenName
diff --git a/tests/Fixtures.hs b/tests/Fixtures.hs
--- a/tests/Fixtures.hs
+++ b/tests/Fixtures.hs
@@ -1,29 +1,25 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE CPP #-}
 
 module Fixtures where
 
 import Data.Aeson
-import Data.Attoparsec.ByteString
-import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
-import Data.Maybe (fromJust)
-import Text.Shakespeare.Text
-
-fj :: T.Text -> Value
-fj = fromJust . maybeResult . parse json . T.encodeUtf8
-
-errorMsgJson :: Value
-errorMsgJson = fj [st|{"request":"\/1\/statuses\/user_timeline.json","error":"Not authorized"}|]
+import Data.ByteString as S
+import System.FilePath
+import Test.Tasty.HUnit
 
-statusJson :: Value
-statusJson = fj [st|{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false}|]
+fixturePath :: FilePath -> FilePath
+fixturePath filename = takeDirectory __FILE__ </> "fixtures" </> filename
 
-statusEntityJson :: Value
-statusEntityJson = fj [st|{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[{"text":"tcdisrupt","indices":[32,42]}],"urls":[{"url":"http:\/\/t.co\/6J2EgYM","expanded_url":"http:\/\/instagr.am\/p\/MuW67\/","display_url":"instagr.am\/p\/MuW67\/","indices":[67,86]}],"user_mentions":[{"screen_name":"twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[0,8]},{"screen_name":"boscomonkey","name":"Bosco So","id":14792670,"id_str":"14792670","indices":[46,58]},{"screen_name":"episod","name":"Taylor Singletary","id":819797,"id_str":"819797","indices":[59,66]}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}|]
+readFixtureFile :: FilePath -> IO ByteString
+readFixtureFile = S.readFile . fixturePath
 
-mediaEntityJson :: Value
-mediaEntityJson = fj [st|{"created_at":"Wed Sep 14 20:58:04 +0000 2011","id":114080493036773378,"id_str":"114080493036773378","text":"Hello America! http:\/\/t.co\/rJC5Pxsu","source":"web","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":373487136,"id_str":"373487136","name":"Y U No @rno?","screen_name":"yunorno","location":"Paris, France","url":"http:\/\/twitter.com\/rno","description":"Oui oui!","protected":false,"followers_count":32,"friends_count":3,"listed_count":2,"created_at":"Wed Sep 14 17:24:54 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":27,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":4,"entities":{"hashtags":[],"urls":[],"user_mentions":[],"media":[{"id":114080493040967680,"id_str":"114080493040967680","indices":[15,35],"media_url":"http:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","url":"http:\/\/t.co\/rJC5Pxsu","display_url":"pic.twitter.com\/rJC5Pxsu","expanded_url":"http:\/\/twitter.com\/yunorno\/status\/114080493036773378\/photo\/1","type":"photo","sizes":{"large":{"w":226,"h":238,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":226,"h":238,"resize":"fit"},"small":{"w":226,"h":238,"resize":"fit"}}}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}|]
+withFixtureJSON :: FromJSON a => FilePath -> (a -> Assertion) -> Assertion
+withFixtureJSON filename assertions = do
+    body <- readFixtureFile filename
+    withJSON body assertions
 
-mediaExtendedEntityJson :: Value
-mediaExtendedEntityJson = fj [st|{"in_reply_to_status_id":null,"id_str":"479666034073296896","truncated":false,"possibly_sensitive":false,"in_reply_to_screen_name":null,"extended_entities":{"media":[{"id_str":"479665892922376192","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","indices":[36,58],"id":479665892922376192,"media_url":"http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":604},"large":{"w":576,"resize":"fit","h":1024},"medium":{"w":576,"resize":"fit","h":1024},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665897150234624","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/Bqgdl5KCQAA9g9V.jpg","indices":[36,58],"id":479665897150234624,"media_url":"http://pbs.twimg.com/media/Bqgdl5KCQAA9g9V.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":599,"resize":"fit","h":337},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665901545852929","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdmJiCEAEp0EI.jpg","indices":[36,58],"id":479665901545852929,"media_url":"http://pbs.twimg.com/media/BqgdmJiCEAEp0EI.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":225},"large":{"w":1024,"resize":"fit","h":678},"medium":{"w":599,"resize":"fit","h":397},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665905375256576","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdmXzCIAAa2lU.jpg","indices":[36,58],"id":479665905375256576,"media_url":"http://pbs.twimg.com/media/BqgdmXzCIAAa2lU.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":225},"large":{"w":1024,"resize":"fit","h":678},"medium":{"w":599,"resize":"fit","h":397},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"}]},"entities":{"symbols":[],"urls":[],"media":[{"id_str":"479665892922376192","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","indices":[36,58],"id":479665892922376192,"media_url":"http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":604},"large":{"w":576,"resize":"fit","h":1024},"medium":{"w":576,"resize":"fit","h":1024},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"}],"user_mentions":[],"hashtags":[{"text":"testnyan","indices":[26,35]}]},"text":"multiple image tweet test #testnyan http://t.co/qOjPwmgLKO","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura_test","is_translation_enabled":false,"default_profile":true,"profile_image_url":"http://abs.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","default_profile_image":true,"id_str":"2418883074","profile_background_image_url_https":"https://abs.twimg.com/images/themes/theme1/bg.png","protected":false,"location":"","entities":{"description":{"urls":[]}},"profile_background_color":"C0DEED","utc_offset":null,"url":null,"profile_text_color":"333333","profile_image_url_https":"https://abs.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","verified":false,"statuses_count":5,"profile_background_tile":false,"following":false,"lang":"ja","follow_request_sent":false,"profile_sidebar_fill_color":"DDEEF6","time_zone":null,"name":"thimura test","profile_sidebar_border_color":"C0DEED","geo_enabled":false,"listed_count":0,"contributors_enabled":false,"created_at":"Sun Mar 30 11:47:17 +0000 2014","id":2418883074,"friends_count":2,"is_translator":false,"favourites_count":0,"notifications":false,"profile_background_image_url":"http://abs.twimg.com/images/themes/theme1/bg.png","profile_use_background_image":true,"description":"","profile_link_color":"0084B4","followers_count":2},"lang":"et","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Thu Jun 19 16:44:28 +0000 2014","source":"\u003ca href=\"https://twitter.com/thimura\" rel=\"nofollow\"\u003e二階堂真紅\u003c/a\u003e","geo":null,"id":479666034073296896,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null}|]
+withJSON :: FromJSON a => ByteString -> (a -> Assertion) -> Assertion
+withJSON body assertions = do
+    case eitherDecodeStrict' body of
+        Left err -> assertFailure $ err
+        Right result -> assertions result
diff --git a/tests/Instances.hs b/tests/Instances.hs
new file mode 100644
--- /dev/null
+++ b/tests/Instances.hs
@@ -0,0 +1,180 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Instances where
+
+import Control.Applicative
+import Data.Aeson
+
+#if MIN_VERSION_aeson(2, 0, 0)
+import Data.Aeson.KeyMap as KeyMap
+#else
+import Data.HashMap.Strict as KeyMap
+#endif
+import Data.HashMap.Strict as HashMap
+import Data.String
+import qualified Data.Text as T
+import Data.Time (UTCTime (..), defaultTimeLocale, fromGregorian, readTime)
+import Generic.Random
+import Test.Tasty.QuickCheck
+import Web.Twitter.Types
+
+instance IsString UTCTime where
+    fromString = readTime defaultTimeLocale twitterTimeFormat
+
+instance Arbitrary UTCTime where
+    arbitrary =
+        do
+            randomDay <- choose (1, 29) :: Gen Int
+            randomMonth <- choose (1, 12) :: Gen Int
+            randomYear <- choose (2001, 2002) :: Gen Integer
+            randomTime <- choose (0, 86401) :: Gen Int
+            return $ UTCTime (fromGregorian randomYear randomMonth randomDay) (fromIntegral randomTime)
+
+instance Arbitrary T.Text where
+    arbitrary = T.pack <$> arbitrary
+
+instance Arbitrary Value where
+    arbitrary =
+        elements
+            [ Object KeyMap.empty
+            , Object (KeyMap.fromList [("test", Number 2), ("value", String "non empty")])
+            ]
+
+-- derive makeArbitrary ''StreamingAPI
+
+instance Arbitrary Status where
+    arbitrary = do
+        qt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)
+        rt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)
+        Status <$> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> pure Nothing
+            <*> pure (statusId <$> qt)
+            <*> pure qt
+            <*> arbitrary
+            <*> arbitrary
+            <*> pure rt
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+
+instance Arbitrary SearchStatus where
+    arbitrary = genericArbitraryU
+instance Arbitrary SearchMetadata where
+    arbitrary = genericArbitraryU
+instance Arbitrary RetweetedStatus where
+    arbitrary = genericArbitraryU
+
+instance Arbitrary DirectMessage where
+    arbitrary = genericArbitrarySingleG customGens
+      where
+        customGens :: Gen Integer :+ ()
+        customGens =
+            (getNonNegative <$> arbitrary) :+ ()
+
+instance Arbitrary EventTarget where
+    arbitrary = genericArbitraryU
+instance Arbitrary Event where
+    arbitrary = genericArbitraryU
+instance Arbitrary Delete where
+    arbitrary = genericArbitraryU
+instance Arbitrary User where
+    arbitrary = genericArbitraryU
+instance Arbitrary List where
+    arbitrary = genericArbitraryU
+instance Arbitrary HashTagEntity where
+    arbitrary = genericArbitraryU
+instance Arbitrary UserEntity where
+    arbitrary = genericArbitraryU
+instance Arbitrary URLEntity where
+    arbitrary = genericArbitraryU
+
+instance Arbitrary MediaEntity where
+    arbitrary = do
+        ms <- arbitrary
+        MediaEntity
+            <$> arbitrary
+            <*> arbitrary
+            <*> pure (HashMap.fromList [("medium", ms)])
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+
+instance Arbitrary MediaSize where
+    arbitrary = genericArbitraryU
+instance Arbitrary Coordinates where
+    arbitrary = genericArbitraryU
+
+instance Arbitrary Place where
+    arbitrary = do
+        Place HashMap.empty
+            <$> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+
+instance Arbitrary BoundingBox where
+    arbitrary = genericArbitraryU
+instance Arbitrary Entities where
+    arbitrary = genericArbitraryU
+instance Arbitrary ExtendedEntities where
+    arbitrary = genericArbitraryU
+instance Arbitrary Variant where
+    arbitrary = genericArbitraryU
+instance Arbitrary VideoInfo where
+    arbitrary = genericArbitraryU
+instance Arbitrary ExtendedEntity where
+    arbitrary = do
+        ms <- arbitrary
+        ExtendedEntity
+            <$> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> pure (HashMap.fromList [("medium", ms)])
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+            <*> arbitrary
+
+instance Arbitrary a => Arbitrary (Entity a) where
+    arbitrary = do
+        a <- arbitrary
+        ind <- arbitrary
+        return $ Entity a ind
+
+instance Arbitrary Contributor where
+    arbitrary = genericArbitraryU
+instance Arbitrary ImageSizeType where
+    arbitrary = genericArbitraryU
+instance Arbitrary UploadedMedia where
+    arbitrary = genericArbitraryU
+instance Arbitrary DisplayTextRange where
+    arbitrary = genericArbitraryU
diff --git a/tests/PropFromToJSONTest.hs b/tests/PropFromToJSONTest.hs
new file mode 100644
--- /dev/null
+++ b/tests/PropFromToJSONTest.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module PropFromToJSONTest where
+
+import Data.Aeson
+import qualified Data.Aeson.Types as Aeson
+import Instances ()
+import Test.Tasty
+import Test.Tasty.QuickCheck
+import Test.Tasty.TH
+import Web.Twitter.Types
+
+fromToJSON :: (Eq a, FromJSON a, ToJSON a) => a -> Bool
+fromToJSON obj = case fromJSON . toJSON $ obj of
+    Aeson.Error _ -> False
+    Aeson.Success a -> a == obj
+
+prop_fromToStatus :: Status -> Bool
+prop_fromToStatus = fromToJSON
+
+prop_fromToSearchStatus :: SearchStatus -> Bool
+prop_fromToSearchStatus = fromToJSON
+
+prop_fromToSearchMetadata :: SearchMetadata -> Bool
+prop_fromToSearchMetadata = fromToJSON
+
+prop_fromToRetweetedStatus :: RetweetedStatus -> Bool
+prop_fromToRetweetedStatus = fromToJSON
+
+prop_fromToDirectMessage :: DirectMessage -> Bool
+prop_fromToDirectMessage = fromToJSON
+
+prop_fromToEventTarget :: EventTarget -> Bool
+prop_fromToEventTarget = fromToJSON
+
+prop_fromToEvent :: Event -> Bool
+prop_fromToEvent = fromToJSON
+
+prop_fromToDelete :: Delete -> Bool
+prop_fromToDelete = fromToJSON
+
+prop_fromToUser :: User -> Bool
+prop_fromToUser = fromToJSON
+
+prop_fromToList :: List -> Bool
+prop_fromToList = fromToJSON
+
+prop_fromToHashTagEntity :: HashTagEntity -> Bool
+prop_fromToHashTagEntity = fromToJSON
+
+prop_fromToUserEntity :: UserEntity -> Bool
+prop_fromToUserEntity = fromToJSON
+
+prop_fromToURLEntity :: URLEntity -> Bool
+prop_fromToURLEntity = fromToJSON
+
+prop_fromToMediaEntity :: MediaEntity -> Bool
+prop_fromToMediaEntity = fromToJSON
+
+prop_fromToMediaSize :: MediaSize -> Bool
+prop_fromToMediaSize = fromToJSON
+
+prop_fromToCoordinates :: Coordinates -> Bool
+prop_fromToCoordinates = fromToJSON
+
+prop_fromToPlace :: Place -> Bool
+prop_fromToPlace = fromToJSON
+
+prop_fromToBoundingBox :: BoundingBox -> Bool
+prop_fromToBoundingBox = fromToJSON
+
+prop_fromToEntities :: Entities -> Bool
+prop_fromToEntities = fromToJSON
+
+prop_fromToContributor :: Contributor -> Bool
+prop_fromToContributor = fromToJSON
+
+prop_fromToImageSizeType :: ImageSizeType -> Bool
+prop_fromToImageSizeType = fromToJSON
+
+prop_fromToUploadedMedia :: UploadedMedia -> Bool
+prop_fromToUploadedMedia = fromToJSON
+
+tests :: TestTree
+tests = $(testGroupGenerator)
diff --git a/tests/StatusTest.hs b/tests/StatusTest.hs
new file mode 100644
--- /dev/null
+++ b/tests/StatusTest.hs
@@ -0,0 +1,245 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module StatusTest where
+
+import Data.Aeson
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.HashMap.Strict as M
+import Data.Maybe
+import Fixtures
+import Instances ()
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.TH
+import Web.Twitter.Types
+
+example_compatibility_classic_13995 :: Status -> Assertion
+example_compatibility_classic_13995 obj = do
+    statusCreatedAt obj @?= "Mon Mar 07 15:13:47 +0000 2016"
+    statusId obj @?= 706860403981099008
+    statusText obj @?= "Peek-a-boo! https://t.co/R3P6waHxRa"
+    statusSource obj @?= "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>"
+    statusTruncated obj @?= False
+    statusEntities obj
+        @?= Just
+            ( Entities
+                { enHashTags = []
+                , enUserMentions = []
+                , enURLs = []
+                , enMedia =
+                    [ Entity
+                        { entityBody =
+                            MediaEntity
+                                { meType = "photo"
+                                , meId = 706860403746181121
+                                , meSizes =
+                                    M.fromList
+                                        [
+                                            ( "small"
+                                            , MediaSize
+                                                { msWidth = 340
+                                                , msHeight = 226
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "large"
+                                            , MediaSize
+                                                { msWidth = 1024
+                                                , msHeight = 680
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "medium"
+                                            , MediaSize
+                                                { msWidth = 600
+                                                , msHeight = 398
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "thumb"
+                                            , MediaSize
+                                                { msWidth = 150
+                                                , msHeight = 150
+                                                , msResize = "crop"
+                                                }
+                                            )
+                                        ]
+                                , meMediaURL = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"
+                                , meMediaURLHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"
+                                , meURL =
+                                    URLEntity
+                                        { ueURL = "https://t.co/R3P6waHxRa"
+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"
+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"
+                                        }
+                                }
+                        , entityIndices = [12, 35]
+                        }
+                    ]
+                }
+            )
+    statusExtendedEntities obj
+        @?= Just
+            ( ExtendedEntities
+                { exeMedia =
+                    [ Entity
+                        { entityBody =
+                            ExtendedEntity
+                                { exeID = 706860403746181121
+                                , exeMediaUrl = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"
+                                , exeMediaUrlHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"
+                                , exeSizes =
+                                    M.fromList
+                                        [
+                                            ( "small"
+                                            , MediaSize
+                                                { msWidth = 340
+                                                , msHeight = 226
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "large"
+                                            , MediaSize
+                                                { msWidth = 1024
+                                                , msHeight = 680
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "medium"
+                                            , MediaSize
+                                                { msWidth = 600
+                                                , msHeight = 398
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "thumb"
+                                            , MediaSize
+                                                { msWidth = 150
+                                                , msHeight = 150
+                                                , msResize = "crop"
+                                                }
+                                            )
+                                        ]
+                                , exeType = "photo"
+                                , exeVideoInfo = Nothing
+                                , exeDurationMillis = Nothing
+                                , exeExtAltText = Nothing
+                                , exeURL =
+                                    URLEntity
+                                        { ueURL = "https://t.co/R3P6waHxRa"
+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"
+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"
+                                        }
+                                }
+                        , entityIndices = [12, 35]
+                        }
+                    ]
+                }
+            )
+    statusInReplyToStatusId obj @?= Nothing
+    statusInReplyToUserId obj @?= Nothing
+    statusFavorited obj @?= Just False
+    statusQuotedStatus obj @?= Nothing
+    statusQuotedStatusId obj @?= Nothing
+    statusRetweetCount obj @?= 0
+    (userScreenName . statusUser) obj @?= "jeremycloud"
+    statusRetweetedStatus obj @?= Nothing
+    statusPlace obj @?= Nothing
+    statusFavoriteCount obj @?= 8
+    statusLang obj @?= Just "en"
+    statusPossiblySensitive obj @?= Just False
+    statusCoordinates obj @?= Nothing
+
+case_compatibility_classic_13995 :: Assertion
+case_compatibility_classic_13995 = do
+    withFixtureJSON "tweet-updates/compatibility_classic_13995.json" $ example_compatibility_classic_13995
+    withFixtureJSON "tweet-updates/compatibility_classic_13995_extended.json" $ example_compatibility_classic_13995
+
+-- case_compatibility_classic_hidden_13797 :: Assertion
+-- case_compatibility_classic_hidden_13797 = withFixtureJSON "tweet-updates/compatibility_classic_hidden_13797" $ \obj -> do
+-- case_compatibility_extended_13996 :: Assertion
+-- case_compatibility_extended_13996 = withFixtureJSON "tweet-updates/compatibility_extended_13996" $ \obj -> do
+-- case_compatibilityplus_classic_13994 :: Assertion
+-- case_compatibilityplus_classic_13994 = withFixtureJSON "tweet-updates/compatibilityplus_classic_13994" $ \obj -> do
+-- case_compatibilityplus_classic_hidden_13797 :: Assertion
+-- case_compatibilityplus_classic_hidden_13797 = withFixtureJSON "tweet-updates/compatibilityplus_classic_hidden_13797" $ \obj -> do
+-- case_compatibilityplus_extended_13997 :: Assertion
+-- case_compatibilityplus_extended_13997 = withFixtureJSON "tweet-updates/compatibilityplus_extended_13997" $ \obj -> do
+-- case_extended_classic_14002 :: Assertion
+-- case_extended_classic_14002 = withFixtureJSON "tweet-updates/extended_classic_14002" $ \obj -> do
+
+case_extended_classic_hidden_13761 :: Assertion
+case_extended_classic_hidden_13761 = withFixtureJSON "tweet-updates/extended_classic_hidden_13761.json" $ \obj -> do
+    statusTruncated obj @?= False
+    statusExtendedEntities obj
+        @?= Just
+            ( ExtendedEntities
+                { exeMedia =
+                    [ Entity
+                        { entityBody =
+                            ExtendedEntity
+                                { exeID = 743479379079004160
+                                , exeMediaUrl = "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"
+                                , exeMediaUrlHttps = "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"
+                                , exeSizes =
+                                    M.fromList
+                                        [
+                                            ( "small"
+                                            , MediaSize
+                                                { msWidth = 340
+                                                , msHeight = 190
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "large"
+                                            , MediaSize
+                                                { msWidth = 480
+                                                , msHeight = 268
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "medium"
+                                            , MediaSize
+                                                { msWidth = 480
+                                                , msHeight = 268
+                                                , msResize = "fit"
+                                                }
+                                            )
+                                        ,
+                                            ( "thumb"
+                                            , MediaSize
+                                                { msWidth = 150
+                                                , msHeight = 150
+                                                , msResize = "crop"
+                                                }
+                                            )
+                                        ]
+                                , exeType = "animated_gif"
+                                , exeVideoInfo = Just (VideoInfo {vsAspectRatio = [120, 67], vsDurationMillis = Nothing, vsVariants = [Variant {vBitrate = Just 0, vContentType = "video/mp4", vUrl = "https://pbs.twimg.com/tweet_video/ClFejl_VAAAo9Xk.mp4"}]})
+                                , exeDurationMillis = Nothing
+                                , exeExtAltText = Nothing
+                                , exeURL = URLEntity {ueURL = "https://t.co/VnJMDg3cbS", ueExpanded = "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1", ueDisplay = "pic.twitter.com/VnJMDg3cbS"}
+                                }
+                        , entityIndices = [48, 71]
+                        }
+                    ]
+                }
+            )
+
+case_extended_extended_14001 :: Assertion
+case_extended_extended_14001 = withFixtureJSON "tweet-updates/extended_extended_14001.json" $ \obj -> do
+    statusText obj @?= "@twitter @twitterdev has more details about these changes at https://t.co/ZnXoRQy8mK.  Thanks for making @twitter more expressive! https://t.co/AWmiH870F7"
+    statusTruncated obj @?= False
+    statusDisplayTextRange obj @?= Just (DisplayTextRange 9 130)
+
+tests :: TestTree
+tests = $(testGroupGenerator)
diff --git a/tests/TypesTest.hs b/tests/TypesTest.hs
--- a/tests/TypesTest.hs
+++ b/tests/TypesTest.hs
@@ -1,81 +1,266 @@
-{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
 
-module Main where
+module TypesTest where
 
+import Data.Aeson
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.HashMap.Strict as M
+import Data.Maybe
+import Data.Time.Clock.POSIX
+import Fixtures
+import Instances ()
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.TH
 import Web.Twitter.Types
 
-import Test.Framework.TH.Prime
-import Test.Framework.Providers.HUnit
-import Test.HUnit
-import Text.Shakespeare.Text
+case_parseStatus :: Assertion
+case_parseStatus = withFixtureJSON "status01.json" $ \obj -> do
+    statusCreatedAt obj @?= "Sat Sep 10 22:23:38 +0000 2011"
+    statusId obj @?= 112652479837110273
+    statusText obj @?= "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM"
+    statusSource obj @?= "<a href=\"http://instagr.am\" rel=\"nofollow\">Instagram</a>"
+    statusTruncated obj @?= False
+    statusEntities obj @?= Nothing
+    statusExtendedEntities obj @?= Nothing
+    statusInReplyToStatusId obj @?= Nothing
+    statusInReplyToUserId obj @?= Just 783214
+    statusFavorited obj @?= Just False
+    statusQuotedStatus obj @?= Nothing
+    statusQuotedStatusId obj @?= Nothing
+    statusRetweetCount obj @?= 0
+    (userScreenName . statusUser) obj @?= "imeoin"
+    statusRetweetedStatus obj @?= Nothing
+    statusPlace obj @?= Nothing
+    statusFavoriteCount obj @?= 0
+    statusLang obj @?= Nothing
+    statusPossiblySensitive obj @?= Just False
+    statusCoordinates obj @?= Nothing
 
-import Data.Aeson hiding (Error)
-import Data.Aeson.Types (parseEither)
-import qualified Data.HashMap.Strict as M
-import Data.Maybe
+case_parseStatusQuoted :: Assertion
+case_parseStatusQuoted = withFixtureJSON "status_quoted.json" $ \obj -> do
+    statusId obj @?= 641660763770372100
+    statusText obj @?= "Wow! Congrats! https://t.co/EPMMldEcci"
+    statusQuotedStatusId obj @?= Just 641653574284537900
 
-import Fixtures
+    let qs = fromJust $ statusQuotedStatus obj
+    statusCreatedAt qs @?= "Wed Sep 09 16:45:08 +0000 2015"
+    statusId qs @?= 641653574284537900
+    statusText qs @?= "Very happy to say that I'm joining @mesosphere as a Distributed Systems Engineer!"
+    statusSource qs @?= "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>"
 
-main :: IO ()
-main = $(defaultMainGenerator)
+    let ent = fromJust $ statusEntities qs
+    enURLs ent @?= []
+    enMedia ent @?= []
+    enHashTags ent @?= []
+    map (userEntityUserId . entityBody) (enUserMentions ent) @?= [1872399366]
+    map (userEntityUserScreenName . entityBody) (enUserMentions ent) @?= ["mesosphere"]
 
-withJSON :: FromJSON a
-         => Value
-         -> (a -> Assertion)
-         -> Assertion
-withJSON js f = either assertFailure id $ do
-  o <- parseEither parseJSON js
-  return $ f o
+    statusExtendedEntities qs @?= Nothing
+    statusInReplyToStatusId qs @?= Nothing
+    statusInReplyToUserId qs @?= Nothing
+    statusFavorited qs @?= Just False
+    statusQuotedStatus qs @?= Nothing
+    statusQuotedStatusId qs @?= Nothing
+    statusRetweetCount qs @?= 7
+    (userScreenName . statusUser) qs @?= "neil_conway"
+    statusRetweeted qs @?= Just False
+    statusRetweetedStatus qs @?= Nothing
+    statusPlace qs @?= Nothing
+    statusFavoriteCount qs @?= 63
+    statusLang qs @?= Just "en"
+    statusPossiblySensitive qs @?= Nothing
+    statusCoordinates qs @?= Nothing
 
-case_parseStatus :: Assertion
-case_parseStatus = withJSON statusJson $ \obj -> do
-  statusId obj @?= 112652479837110273
-  statusRetweetCount obj @?= Just 0
-  (userScreenName . statusUser) obj @?= "imeoin"
-  statusEntities obj @?= Nothing
+case_parseStatusWithPhoto :: Assertion
+case_parseStatusWithPhoto = withFixtureJSON "status_thimura_with_photo.json" $ \obj -> do
+    statusId obj @?= 491143410770657280
+    statusText obj @?= "近所の海です http://t.co/FjSOU8dDoD"
+    statusTruncated obj @?= False
 
+    let ent = fromJust $ statusEntities obj
+    enHashTags ent @?= []
+    enUserMentions ent @?= []
+    enURLs ent @?= []
+    length (enMedia ent) @?= 1
+    map (meMediaURLHttps . entityBody) (enMedia ent) @?= ["https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg"]
+
+    let exents = fromJust $ statusExtendedEntities obj
+    let media = exeMedia exents
+    length media @?= 1
+    let exent = entityBody $ head media
+    exeID exent @?= 491143397378244610
+    ueURL (exeURL exent) @?= "http://t.co/FjSOU8dDoD"
+
+    statusInReplyToStatusId obj @?= Nothing
+    statusInReplyToUserId obj @?= Nothing
+    statusFavorited obj @?= Just False
+    statusRetweetCount obj @?= 4
+    (userScreenName . statusUser) obj @?= "thimura"
+    statusRetweetedStatus obj @?= Nothing
+    statusPlace obj @?= Nothing
+    statusFavoriteCount obj @?= 9
+    statusLang obj @?= Just "ja"
+    statusPossiblySensitive obj @?= Just False
+    statusCoordinates obj @?= Nothing
+
 case_parseStatusIncludeEntities :: Assertion
-case_parseStatusIncludeEntities = withJSON statusEntityJson $ \obj -> do
-  statusId obj @?= 112652479837110273
-  statusRetweetCount obj @?= Just 0
-  (userScreenName . statusUser) obj @?= "imeoin"
-  let ent = fromMaybe (Entities [] [] [] []) $ statusEntities obj
-  (map entityIndices . enHashTags) ent @?= [[32,42]]
-  (hashTagText . entityBody . head . enHashTags) ent @?= "tcdisrupt"
+case_parseStatusIncludeEntities = withFixtureJSON "status_with_entity.json" $ \obj -> do
+    statusId obj @?= 112652479837110273
+    statusRetweetCount obj @?= 0
+    (userScreenName . statusUser) obj @?= "imeoin"
+    let ent = fromMaybe (Entities [] [] [] []) $ statusEntities obj
+    (map entityIndices . enHashTags) ent @?= [[32, 42]]
+    (hashTagText . entityBody . head . enHashTags) ent @?= "tcdisrupt"
 
+case_parseSearchStatusMetadata :: Assertion
+case_parseSearchStatusMetadata = withFixtureJSON "search_haskell.json" $ \obj -> do
+    let status = (searchResultStatuses obj) :: [Status]
+    length status @?= 1
+
+    let metadata = searchResultSearchMetadata obj
+    searchMetadataMaxId metadata @?= 495597397733433345
+    searchMetadataSinceId metadata @?= 0
+    searchMetadataRefreshURL metadata @?= "?since_id=495597397733433345&q=haskell&include_entities=1"
+    searchMetadataNextResults metadata @?= Just "?max_id=495594369802440705&q=haskell&include_entities=1"
+    searchMetadataCount metadata @?= 1
+    searchMetadataCompletedIn metadata @?= Just 0.043
+    searchMetadataSinceIdStr metadata @?= "0"
+    searchMetadataQuery metadata @?= "haskell"
+    searchMetadataMaxIdStr metadata @?= "495597397733433345"
+
+case_parseSearchStatusBodyStatus :: Assertion
+case_parseSearchStatusBodyStatus = withFixtureJSON "search_haskell.json" $ \obj -> do
+    let status = (searchResultStatuses obj) :: [Status]
+    length status @?= 1
+    statusText (head status) @?= "haskell"
+
+case_parseSearchStatusBodySearchStatus :: Assertion
+case_parseSearchStatusBodySearchStatus = withFixtureJSON "search_haskell.json" $ \obj -> do
+    let status = (searchResultStatuses obj) :: [SearchStatus]
+    length status @?= 1
+    searchStatusText (head status) @?= "haskell"
+
+data DMList = DMList
+    { dmList :: [DirectMessage]
+    }
+    deriving (Show, Eq)
+instance FromJSON DMList where
+    parseJSON = withObject "DMList" $ \obj -> DMList <$> obj .: "events"
+
+case_parseDirectMessageList :: Assertion
+case_parseDirectMessageList =
+    withFixtureJSON "direct_message_event_list.json" $ \obj -> do
+        dmList obj
+            @?= [ DirectMessage
+                    { dmId = 123123123123123123
+                    , dmCreatedTimestamp = read "2019-10-13 18:15:48.951 UTC"
+                    , dmTargetRecipientId = 186712193
+                    , dmSenderId = 69179963
+                    , dmText = "hello @thimura"
+                    , dmEntities =
+                        Entities
+                            { enHashTags = []
+                            , enUserMentions =
+                                [ Entity
+                                    { entityBody =
+                                        UserEntity
+                                            { userEntityUserId = 69179963
+                                            , userEntityUserName = "ちむら"
+                                            , userEntityUserScreenName = "thimura"
+                                            }
+                                    , entityIndices = [6, 14]
+                                    }
+                                ]
+                            , enURLs = []
+                            , enMedia = []
+                            }
+                    }
+                , DirectMessage
+                    { dmId = 25252525252525
+                    , dmCreatedTimestamp = read "2019-10-13 18:06:46.14 UTC"
+                    , dmTargetRecipientId = 186712193
+                    , dmSenderId = 69179963
+                    , dmText = "hello"
+                    , dmEntities = Entities {enHashTags = [], enUserMentions = [], enURLs = [], enMedia = []}
+                    }
+                ]
+
+case_parseEventFavorite :: Assertion
+case_parseEventFavorite = withFixtureJSON "event_favorite_thimura.json" $ \obj -> do
+    evCreatedAt obj @?= "Sat Aug 02 16:32:01 +0000 2014"
+    evEvent obj @?= "favorite"
+    let Just (ETStatus targetObj) = evTargetObject obj
+    statusId targetObj @?= 495597326736449536
+    statusText targetObj @?= "haskell"
+
+    let ETUser targetUser = evTarget obj
+    userScreenName targetUser @?= "thimura"
+
+    let ETUser sourceUser = evSource obj
+    userScreenName sourceUser @?= "thimura_shinku"
+
+case_parseEventUnfavorite :: Assertion
+case_parseEventUnfavorite = withFixtureJSON "event_unfavorite_thimura.json" $ \obj -> do
+    evCreatedAt obj @?= "Sat Aug 02 16:32:10 +0000 2014"
+    evEvent obj @?= "unfavorite"
+    let Just (ETStatus targetObj) = evTargetObject obj
+    statusId targetObj @?= 495597326736449536
+    statusText targetObj @?= "haskell"
+
+    let ETUser targetUser = evTarget obj
+    userScreenName targetUser @?= "thimura"
+
+    let ETUser sourceUser = evSource obj
+    userScreenName sourceUser @?= "thimura_shinku"
+
+case_parseDelete :: Assertion
+case_parseDelete = withFixtureJSON "delete.json" $ \obj -> do
+    delId obj @?= 495607981833064448
+    delUserId obj @?= 2566877347
+
 case_parseErrorMsg :: Assertion
-case_parseErrorMsg =
-  case parseStatus errorMsgJson of
-    Left str -> "Not authorized" @=? str
-    Right _ -> assertFailure "errorMsgJson should be parsed as an error."
+case_parseErrorMsg = withFixtureJSON "error_not_authorized.json" $ \value ->
+    case parseStatus value of
+        Aeson.Error str -> "Not authorized" @=? str
+        Aeson.Success _ -> assertFailure "errorMsgJson should be parsed as an error."
   where
-    parseStatus :: Value -> Either String Status
-    parseStatus = parseEither parseJSON
+    parseStatus :: Value -> Aeson.Result Status
+    parseStatus = Aeson.parse parseJSON
 
 case_parseMediaEntity :: Assertion
-case_parseMediaEntity = withJSON mediaEntityJson $ \obj -> do
-  let entities = statusEntities obj
-  assert $ isJust entities
-  let Just ent = entities
-      media = enMedia ent
-  length media @?= 1
-  let me = entityBody $ head media
-  ueURL (meURL me) @?= "http://t.co/rJC5Pxsu"
-  meMediaURLHttps me @?= "https://pbs.twimg.com/media/AZVLmp-CIAAbkyy.jpg"
-  let sizes = meSizes me
-  assert $ M.member "thumb" sizes
-  assert $ M.member "large" sizes
+case_parseMediaEntity = withFixtureJSON "media_entity.json" $ \obj -> do
+    let entities = statusEntities obj
+    assertBool "entities should not empty" $ isJust entities
+    let Just ent = entities
+        media = enMedia ent
+    length media @?= 1
+    let me = entityBody $ head media
+    meType me @?= "photo"
+    meId me @?= 114080493040967680
+    let sizes = meSizes me
+    assertBool "sizes must contains \"thumb\"" $ M.member "thumb" sizes
+    assertBool "sizes must contains \"large\"" $ M.member "large" sizes
 
+    let Just mediaSize = M.lookup "large" sizes
+
+    msWidth mediaSize @?= 226
+    msHeight mediaSize @?= 238
+    msResize mediaSize @?= "fit"
+
+    ueURL (meURL me) @?= "http://t.co/rJC5Pxsu"
+    meMediaURLHttps me @?= "https://pbs.twimg.com/media/AZVLmp-CIAAbkyy.jpg"
+
 case_parseEmptyEntity :: Assertion
-case_parseEmptyEntity = withJSON (fj [st|{}|]) $ \entity -> do
+case_parseEmptyEntity = withJSON "{}" $ \entity -> do
     length (enHashTags entity) @?= 0
     length (enUserMentions entity) @?= 0
     length (enURLs entity) @?= 0
     length (enMedia entity) @?= 0
 
 case_parseEntityHashTag :: Assertion
-case_parseEntityHashTag = withJSON (fj [st|{"symbols":[],"urls":[{"indices":[32,52], "url":"http://t.co/IOwBrTZR", "display_url":"youtube.com/watch?v=oHg5SJ\u2026", "expanded_url":"http://www.youtube.com/watch?v=oHg5SJYRHA0"}],"user_mentions":[{"name":"Twitter API", "indices":[4,15], "screen_name":"twitterapi", "id":6253282, "id_str":"6253282"}],"hashtags":[{"indices":[32,36],"text":"lol"}]}|]) $ \entity -> do
+case_parseEntityHashTag = withFixtureJSON "entity01.json" $ \entity -> do
     length (enHashTags entity) @?= 1
     length (enUserMentions entity) @?= 1
     length (enURLs entity) @?= 1
@@ -95,12 +280,62 @@
     hashtag @?= "lol"
 
 case_parseExtendedEntities :: Assertion
-case_parseExtendedEntities = withJSON mediaExtendedEntityJson $ \obj -> do
+case_parseExtendedEntities = withFixtureJSON "media_extended_entity.json" $ \obj -> do
     let entities = statusExtendedEntities obj
-    assert $ isJust entities
+    assertBool "entities should not empty" $ isJust entities
     let Just ent = entities
-        media = enMedia ent
+        media = exeMedia ent
     length media @?= 4
     let me = entityBody $ head media
-    ueURL (meURL me) @?= "http://t.co/qOjPwmgLKO"
-    meMediaURL me @?= "http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg"
+    ueURL (exeURL me) @?= "https://t.co/Qi316FhOwe"
+    exeMediaUrl me @?= "http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg"
+    exeExtAltText me @?= Just "A small tabby kitten"
+    exeType me @?= "photo"
+
+case_parseUser :: Assertion
+case_parseUser = withFixtureJSON "user_thimura.json" $ \obj -> do
+    userId obj @?= 69179963
+    userName obj @?= "ちむら"
+    userScreenName obj @?= "thimura"
+    userDescription obj @?= Just "真紅かわいい"
+    userLocation obj @?= Just "State# Irotoridori.No.World"
+    userProfileImageURL obj @?= Just "http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png"
+    userURL obj @?= Just "http://t.co/TFUAsAffX0"
+    userProtected obj @?= False
+    userFollowersCount obj @?= 754
+    userFriendsCount obj @?= 780
+    userStatusesCount obj @?= 24709
+    userLang obj @?= Just "en"
+    userCreatedAt obj @?= "Thu Aug 27 02:48:06 +0000 2009"
+    userFavoritesCount obj @?= 17313
+
+case_parseUserLangNull :: Assertion
+case_parseUserLangNull = withFixtureJSON "user_thimura_lang_null.json" $ \obj -> do
+    userId obj @?= 69179963
+    userName obj @?= "ちむら"
+    userScreenName obj @?= "thimura"
+    userDescription obj @?= Just "真紅かわいい"
+    userLocation obj @?= Just "State# Irotoridori.No.World"
+    userProfileImageURL obj @?= Just "http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png"
+    userURL obj @?= Just "http://t.co/TFUAsAffX0"
+    userProtected obj @?= False
+    userFollowersCount obj @?= 754
+    userFriendsCount obj @?= 780
+    userStatusesCount obj @?= 24709
+    -- N.B. the only difference between this one and case_parseUser is in lang
+    userLang obj @?= Nothing
+    userCreatedAt obj @?= "Thu Aug 27 02:48:06 +0000 2009"
+    userFavoritesCount obj @?= 17313
+
+case_parseList :: Assertion
+case_parseList = withFixtureJSON "list_thimura_haskell.json" $ \obj -> do
+    listId obj @?= 20849097
+    listName obj @?= "haskell"
+    listFullName obj @?= "@thimura/haskell"
+    listMemberCount obj @?= 50
+    listSubscriberCount obj @?= 1
+    listMode obj @?= "public"
+    (userScreenName . listUser) obj @?= "thimura"
+
+tests :: TestTree
+tests = $(testGroupGenerator)
diff --git a/tests/fixtures/delete.json b/tests/fixtures/delete.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/delete.json
@@ -0,0 +1,1 @@
+{"delete":{"status":{"id":495607981833064448,"user_id":2566877347,"id_str":"495607981833064448","user_id_str":"2566877347"}}}
diff --git a/tests/fixtures/direct_message_event_list.json b/tests/fixtures/direct_message_event_list.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/direct_message_event_list.json
@@ -0,0 +1,69 @@
+{
+  "apps": {
+    "258901": {
+      "url": "http://twitter.com/download/android",
+      "name": "Twitter for Android",
+      "id": "258901"
+    },
+    "3033300": {
+      "url": "https://mobile.twitter.com",
+      "name": "Twitter Web App",
+      "id": "3033300"
+    }
+  },
+  "events": [
+    {
+      "message_create": {
+        "source_app_id": "3033300",
+        "message_data": {
+          "entities": {
+            "symbols": [],
+            "urls": [],
+            "user_mentions": [
+              {
+                "screen_name": "thimura",
+                "id_str": "69179963",
+                "name": "ちむら",
+                "indices": [
+                  6,
+                  14
+                ],
+                "id": 69179963
+              }
+            ],
+            "hashtags": []
+          },
+          "text": "hello @thimura"
+        },
+        "target": {
+          "recipient_id": "186712193"
+        },
+        "sender_id": "69179963"
+      },
+      "created_timestamp": "1570990548951",
+      "id": "123123123123123123",
+      "type": "message_create"
+    },
+    {
+      "message_create": {
+        "source_app_id": "4237790",
+        "message_data": {
+          "entities": {
+            "symbols": [],
+            "urls": [],
+            "user_mentions": [],
+            "hashtags": []
+          },
+          "text": "hello"
+        },
+        "target": {
+          "recipient_id": "186712193"
+        },
+        "sender_id": "69179963"
+      },
+      "created_timestamp": "1570990006140",
+      "id": "25252525252525",
+      "type": "message_create"
+    }
+  ]
+}
diff --git a/tests/fixtures/entity01.json b/tests/fixtures/entity01.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/entity01.json
@@ -0,0 +1,1 @@
+{"symbols":[],"urls":[{"indices":[32,52], "url":"http://t.co/IOwBrTZR", "display_url":"youtube.com/watch?v=oHg5SJ\u2026", "expanded_url":"http://www.youtube.com/watch?v=oHg5SJYRHA0"}],"user_mentions":[{"name":"Twitter API", "indices":[4,15], "screen_name":"twitterapi", "id":6253282, "id_str":"6253282"}],"hashtags":[{"indices":[32,36],"text":"lol"}]}
diff --git a/tests/fixtures/error_not_authorized.json b/tests/fixtures/error_not_authorized.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/error_not_authorized.json
@@ -0,0 +1,1 @@
+{"request":"\/1\/statuses\/user_timeline.json","error":"Not authorized"}
diff --git a/tests/fixtures/event_favorite_thimura.json b/tests/fixtures/event_favorite_thimura.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/event_favorite_thimura.json
@@ -0,0 +1,1 @@
+{"event":"favorite","created_at":"Sat Aug 02 16:32:01 +0000 2014","source":{"id":2566877347,"id_str":"2566877347","name":"thimura shinku","screen_name":"thimura_shinku","location":null,"url":null,"description":"\u4e8c\u968e\u5802 \u771f\u7d05","protected":false,"followers_count":4,"friends_count":3,"listed_count":0,"created_at":"Sat Jun 14 10:15:19 +0000 2014","favourites_count":2,"utc_offset":32400,"time_zone":"Irkutsk","geo_enabled":false,"verified":false,"statuses_count":1,"lang":"ja","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/2566877347\/1402741935","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target_object":{"created_at":"Sat Aug 02 15:49:45 +0000 2014","id":495597326736449536,"id_str":"495597326736449536","text":"haskell","source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorite_count":2,"entities":{"hashtags":[],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"}}
diff --git a/tests/fixtures/event_unfavorite_thimura.json b/tests/fixtures/event_unfavorite_thimura.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/event_unfavorite_thimura.json
@@ -0,0 +1,1 @@
+{"event":"unfavorite","created_at":"Sat Aug 02 16:32:10 +0000 2014","source":{"id":2566877347,"id_str":"2566877347","name":"thimura shinku","screen_name":"thimura_shinku","location":null,"url":null,"description":"\u4e8c\u968e\u5802 \u771f\u7d05","protected":false,"followers_count":4,"friends_count":3,"listed_count":0,"created_at":"Sat Jun 14 10:15:19 +0000 2014","favourites_count":1,"utc_offset":32400,"time_zone":"Irkutsk","geo_enabled":false,"verified":false,"statuses_count":2,"lang":"ja","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/2566877347\/1402741935","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target_object":{"created_at":"Sat Aug 02 15:49:45 +0000 2014","id":495597326736449536,"id_str":"495597326736449536","text":"haskell","source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":1,"favorite_count":1,"entities":{"hashtags":[],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"}}
diff --git a/tests/fixtures/list_thimura_haskell.json b/tests/fixtures/list_thimura_haskell.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/list_thimura_haskell.json
@@ -0,0 +1,1 @@
+{"id_str":"20849097","slug":"haskell","mode":"public","full_name":"@thimura/haskell","uri":"/thimura/haskell","following":true,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24734,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17345,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"member_count":50,"name":"haskell","created_at":"Sat Sep 04 11:48:02 +0000 2010","id":20849097,"subscriber_count":1,"description":""}
diff --git a/tests/fixtures/media_entity.json b/tests/fixtures/media_entity.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/media_entity.json
@@ -0,0 +1,1 @@
+{"created_at":"Wed Sep 14 20:58:04 +0000 2011","id":114080493036773378,"id_str":"114080493036773378","text":"Hello America! http:\/\/t.co\/rJC5Pxsu","source":"web","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":373487136,"id_str":"373487136","name":"Y U No @rno?","screen_name":"yunorno","location":"Paris, France","url":"http:\/\/twitter.com\/rno","description":"Oui oui!","protected":false,"followers_count":32,"friends_count":3,"listed_count":2,"created_at":"Wed Sep 14 17:24:54 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":27,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":4,"entities":{"hashtags":[],"urls":[],"user_mentions":[],"media":[{"id":114080493040967680,"id_str":"114080493040967680","indices":[15,35],"media_url":"http:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","url":"http:\/\/t.co\/rJC5Pxsu","display_url":"pic.twitter.com\/rJC5Pxsu","expanded_url":"http:\/\/twitter.com\/yunorno\/status\/114080493036773378\/photo\/1","type":"photo","sizes":{"large":{"w":226,"h":238,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":226,"h":238,"resize":"fit"},"small":{"w":226,"h":238,"resize":"fit"}}}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}
diff --git a/tests/fixtures/media_extended_entity.json b/tests/fixtures/media_extended_entity.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/media_extended_entity.json
@@ -0,0 +1,1 @@
+{"created_at":"Fri Jul 29 20:14:40 +0000 2016","id":759119974329823200,"id_str":"759119974329823233","text":"look at all these cats tho https://t.co/Qi316FhOwe","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[],"media":[{"id":759119267472089100,"id_str":"759119267472089089","indices":[27,50],"media_url":"http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","media_url_https":"https://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"large":{"w":300,"h":400,"resize":"fit"},"medium":{"w":300,"h":400,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":300,"h":400,"resize":"fit"}}}]},"extended_entities":{"media":[{"id":759119267472089100,"id_str":"759119267472089089","indices":[27,50],"media_url":"http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","media_url_https":"https://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"large":{"w":300,"h":400,"resize":"fit"},"medium":{"w":300,"h":400,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":300,"h":400,"resize":"fit"}},"ext_alt_text":"A small tabby kitten"},{"id":759119396350505000,"id_str":"759119396350504960","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvEamUsAARiX2.jpg","media_url_https":"https://pbs.twimg.com/media/CojvEamUsAARiX2.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"medium":{"w":500,"h":301,"resize":"fit"},"small":{"w":500,"h":301,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"large":{"w":500,"h":301,"resize":"fit"}},"ext_alt_text":"four kittens sitting together"},{"id":759119476621086700,"id_str":"759119476621086720","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvJFoUkAAoWIR.jpg","media_url_https":"https://pbs.twimg.com/media/CojvJFoUkAAoWIR.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"thumb":{"w":150,"h":150,"resize":"crop"},"large":{"w":500,"h":600,"resize":"fit"},"medium":{"w":500,"h":600,"resize":"fit"},"small":{"w":500,"h":600,"resize":"fit"}},"ext_alt_text":"a cute soft kitten with a red ornament"},{"id":759119583466758100,"id_str":"759119583466758145","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvPTqUIAEYF7l.jpg","media_url_https":"https://pbs.twimg.com/media/CojvPTqUIAEYF7l.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"medium":{"w":650,"h":600,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":650,"h":600,"resize":"fit"},"large":{"w":650,"h":600,"resize":"fit"}},"ext_alt_text":"a skeptical-looking tabby cat outside"}]},"source":"<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":753372895682949100,"id_str":"753372895682949120","name":"Please Caption!","screen_name":"PleaseCaption","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":1,"listed_count":0,"created_at":"Wed Jul 13 23:37:50 +0000 2016","favourites_count":0,"utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":7,"lang":"en-gb","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"F5F8FA","profile_background_image_url":null,"profile_background_image_url_https":null,"profile_background_tile":false,"profile_image_url":"http://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","profile_image_url_https":"https://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","profile_link_color":"2B7BB9","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":true,"default_profile_image":true,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false,"possibly_sensitive_appealable":false,"lang":"en"}
diff --git a/tests/fixtures/search_haskell.json b/tests/fixtures/search_haskell.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/search_haskell.json
@@ -0,0 +1,1 @@
+{"search_metadata":{"next_results":"?max_id=495594369802440705&q=haskell&include_entities=1","since_id_str":"0","completed_in":0.043,"count":1,"refresh_url":"?since_id=495597397733433345&q=haskell&include_entities=1","max_id_str":"495597397733433345","query":"haskell","since_id":0,"max_id":495597397733433345},"statuses":[{"in_reply_to_status_id":null,"id_str":"495597326736449536","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"haskell","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24747,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17362,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"lang":"et","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Sat Aug 02 15:49:45 +0000 2014","metadata":{"result_type":"recent","iso_language_code":"et"},"source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495597326736449536,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null}]}
diff --git a/tests/fixtures/status01.json b/tests/fixtures/status01.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/status01.json
@@ -0,0 +1,1 @@
+{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false}
diff --git a/tests/fixtures/status_quoted.json b/tests/fixtures/status_quoted.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/status_quoted.json
@@ -0,0 +1,202 @@
+{
+  "created_at": "Wed Sep 09 17:13:42 +0000 2015",
+  "id": 641660763770372100,
+  "id_str": "641660763770372097",
+  "text": "Wow! Congrats! https://t.co/EPMMldEcci",
+  "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
+  "truncated": false,
+  "in_reply_to_status_id": null,
+  "in_reply_to_status_id_str": null,
+  "in_reply_to_user_id": null,
+  "in_reply_to_user_id_str": null,
+  "in_reply_to_screen_name": null,
+  "user":  {
+    "id": 15232432,
+    "id_str": "15232432",
+    "name": "Dean Wampler",
+    "screen_name": "deanwampler",
+    "location": "Chicago",
+    "description": "Minor irritant, major pedant, Big Data, IoT, and Scala poser. Lurks at Typesafe. O'Reilly author. Opinions are my own..",
+    "url": "http://t.co/bzGTwCnmvO",
+    "entities":  {
+      "url":  {
+        "urls":  [
+           {
+            "url": "http://t.co/bzGTwCnmvO",
+            "expanded_url": "http://typesafe.com",
+            "display_url": "typesafe.com",
+            "indices":  [
+              0,
+              22
+            ]
+          }
+        ]
+      },
+      "description":  {
+        "urls":  []
+      }
+    },
+    "protected": false,
+    "followers_count": 7221,
+    "friends_count": 811,
+    "listed_count": 645,
+    "created_at": "Wed Jun 25 15:24:31 +0000 2008",
+    "favourites_count": 2841,
+    "utc_offset": -18000,
+    "time_zone": "Central Time (US & Canada)",
+    "geo_enabled": true,
+    "verified": false,
+    "statuses_count": 13483,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "9AE4E8",
+    "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/670120165/5514a5b016bf4d3d617ece5f117643d0.jpeg",
+    "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/670120165/5514a5b016bf4d3d617ece5f117643d0.jpeg",
+    "profile_background_tile": true,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/639619926374748160/QhODNcWz_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/639619926374748160/QhODNcWz_normal.jpg",
+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15232432/1359038744",
+    "profile_link_color": "0000FF",
+    "profile_sidebar_border_color": "FFFFFF",
+    "profile_sidebar_fill_color": "DFF8A0",
+    "profile_text_color": "070708",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": false,
+    "default_profile_image": false,
+    "following": false,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 641653574284537900,
+  "quoted_status_id_str": "641653574284537856",
+  "quoted_status":  {
+    "created_at": "Wed Sep 09 16:45:08 +0000 2015",
+    "id": 641653574284537900,
+    "id_str": "641653574284537856",
+    "text": "Very happy to say that I'm joining @mesosphere as a Distributed Systems Engineer!",
+    "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
+    "truncated": false,
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user":  {
+      "id": 54129186,
+      "id_str": "54129186",
+      "name": "Neil Conway",
+      "screen_name": "neil_conway",
+      "location": "Oakland, CA",
+      "description": "Large-scale data management and distributed systems.",
+      "url": "http://t.co/grthxRo5Q1",
+      "entities":  {
+        "url":  {
+          "urls":  [
+             {
+              "url": "http://t.co/grthxRo5Q1",
+              "expanded_url": "http://neilconway.org",
+              "display_url": "neilconway.org",
+              "indices":  [
+                0,
+                22
+              ]
+            }
+          ]
+        },
+        "description":  {
+          "urls":  []
+        }
+      },
+      "protected": false,
+      "followers_count": 3380,
+      "friends_count": 680,
+      "listed_count": 202,
+      "created_at": "Mon Jul 06 05:43:15 +0000 2009",
+      "favourites_count": 611,
+      "utc_offset": -25200,
+      "time_zone": "Pacific Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 3559,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "FFFFFF",
+      "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/204823473/Untitled-2.jpg",
+      "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/204823473/Untitled-2.jpg",
+      "profile_background_tile": true,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/299445365/3613789225_66f1cc848f_b_normal.jpg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/299445365/3613789225_66f1cc848f_b_normal.jpg",
+      "profile_link_color": "036E6E",
+      "profile_sidebar_border_color": "B05607",
+      "profile_sidebar_fill_color": "E8F0F0",
+      "profile_text_color": "998308",
+      "profile_use_background_image": true,
+      "has_extended_profile": false,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 7,
+    "favorite_count": 63,
+    "entities":  {
+      "hashtags":  [],
+      "symbols":  [],
+      "user_mentions":  [
+         {
+          "screen_name": "mesosphere",
+          "name": "Mesosphere",
+          "id": 1872399366,
+          "id_str": "1872399366",
+          "indices":  [
+            35,
+            46
+          ]
+        }
+      ],
+      "urls":  []
+    },
+    "favorited": false,
+    "retweeted": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 1,
+  "entities":  {
+    "hashtags":  [],
+    "symbols":  [],
+    "user_mentions":  [],
+    "urls":  [
+       {
+        "url": "https://t.co/EPMMldEcci",
+        "expanded_url": "https://twitter.com/neil_conway/status/641653574284537856",
+        "display_url": "twitter.com/neil_conway/st…",
+        "indices":  [
+          15,
+          38
+        ]
+      }
+    ]
+  },
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/status_thimura_with_photo.json b/tests/fixtures/status_thimura_with_photo.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/status_thimura_with_photo.json
@@ -0,0 +1,1 @@
+{"in_reply_to_status_id":null,"id_str":"491143410770657280","truncated":false,"possibly_sensitive":false,"in_reply_to_screen_name":null,"extended_entities":{"media":[{"id_str":"491143397378244610","expanded_url":"http://twitter.com/thimura/status/491143410770657280/photo/1","url":"http://t.co/FjSOU8dDoD","media_url_https":"https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","indices":[7,29],"id":491143397378244610,"media_url":"http://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":600,"resize":"fit","h":338},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/FjSOU8dDoD"}]},"entities":{"symbols":[],"urls":[],"media":[{"id_str":"491143397378244610","expanded_url":"http://twitter.com/thimura/status/491143410770657280/photo/1","url":"http://t.co/FjSOU8dDoD","media_url_https":"https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","indices":[7,29],"id":491143397378244610,"media_url":"http://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":600,"resize":"fit","h":338},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/FjSOU8dDoD"}],"user_mentions":[],"hashtags":[]},"text":"近所の海です http://t.co/FjSOU8dDoD","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24734,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17345,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"lang":"ja","retweet_count":4,"in_reply_to_user_id":null,"created_at":"Mon Jul 21 08:51:28 +0000 2014","source":"\u003ca href=\"https://sites.google.com/site/wakamesoba98/sobacha\" rel=\"nofollow\"\u003eSobaCha\u003c/a\u003e","geo":null,"id":491143410770657280,"in_reply_to_status_id_str":null,"favorite_count":9,"contributors":null,"place":null}
diff --git a/tests/fixtures/status_with_entity.json b/tests/fixtures/status_with_entity.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/status_with_entity.json
@@ -0,0 +1,1 @@
+{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[{"text":"tcdisrupt","indices":[32,42]}],"urls":[{"url":"http:\/\/t.co\/6J2EgYM","expanded_url":"http:\/\/instagr.am\/p\/MuW67\/","display_url":"instagr.am\/p\/MuW67\/","indices":[67,86]}],"user_mentions":[{"screen_name":"twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[0,8]},{"screen_name":"boscomonkey","name":"Bosco So","id":14792670,"id_str":"14792670","indices":[46,58]},{"screen_name":"episod","name":"Taylor Singletary","id":819797,"id_str":"819797","indices":[59,66]}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}
diff --git a/tests/fixtures/tweet-updates/compatibility_classic_13995.json b/tests/fixtures/tweet-updates/compatibility_classic_13995.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibility_classic_13995.json
@@ -0,0 +1,169 @@
+{
+  "created_at": "Mon Mar 07 15:13:47 +0000 2016",
+  "id": 706860403981099008,
+  "id_str": "706860403981099008",
+  "text": "Peek-a-boo! https://t.co/R3P6waHxRa",
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [],
+    "urls": [],
+    "media": [
+      {
+        "id": 706860403746181121,
+        "id_str": "706860403746181121",
+        "indices": [
+          12,
+          35
+        ],
+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "url": "https://t.co/R3P6waHxRa",
+        "display_url": "pic.twitter.com/R3P6waHxRa",
+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",
+        "type": "photo",
+        "sizes": {
+          "medium": {
+            "w": 600,
+            "h": 398,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 226,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 1024,
+            "h": 680,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "extended_entities": {
+    "media": [
+      {
+        "id": 706860403746181121,
+        "id_str": "706860403746181121",
+        "indices": [
+          12,
+          35
+        ],
+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "url": "https://t.co/R3P6waHxRa",
+        "display_url": "pic.twitter.com/R3P6waHxRa",
+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",
+        "type": "photo",
+        "sizes": {
+          "medium": {
+            "w": 600,
+            "h": 398,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 226,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 1024,
+            "h": 680,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "truncated": false,
+  "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",
+  "in_reply_to_status_id": null,
+  "in_reply_to_status_id_str": null,
+  "in_reply_to_user_id": null,
+  "in_reply_to_user_id_str": null,
+  "in_reply_to_screen_name": null,
+  "user": {
+    "id": 15062340,
+    "id_str": "15062340",
+    "name": "/dev/cloud/jeremy",
+    "screen_name": "jeremycloud",
+    "location": "Madison, Wisconsin",
+    "description": "Professional yak shaver. Amateur bike shedder.",
+    "url": "https://t.co/FcYeBkOpVY",
+    "entities": {
+      "url": {
+        "urls": [
+          {
+            "url": "https://t.co/FcYeBkOpVY",
+            "expanded_url": "http://about.me/jeremy.cloud",
+            "display_url": "about.me/jeremy.cloud",
+            "indices": [
+              0,
+              23
+            ]
+          }
+        ]
+      },
+      "description": {
+        "urls": []
+      }
+    },
+    "protected": false,
+    "followers_count": 4324,
+    "friends_count": 410,
+    "listed_count": 103,
+    "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+    "favourites_count": 815,
+    "utc_offset": -18000,
+    "time_zone": "Central Time (US & Canada)",
+    "geo_enabled": true,
+    "verified": false,
+    "statuses_count": 2218,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "000000",
+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+    "profile_link_color": "4A913C",
+    "profile_sidebar_border_color": "000000",
+    "profile_sidebar_fill_color": "000000",
+    "profile_text_color": "000000",
+    "profile_use_background_image": false,
+    "has_extended_profile": true,
+    "default_profile": false,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "is_quote_status": false,
+  "retweet_count": 0,
+  "favorite_count": 8,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibility_classic_13995_extended.json b/tests/fixtures/tweet-updates/compatibility_classic_13995_extended.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibility_classic_13995_extended.json
@@ -0,0 +1,173 @@
+{
+  "created_at": "Mon Mar 07 15:13:47 +0000 2016",
+  "id": 706860403981099008,
+  "id_str": "706860403981099008",
+  "full_text": "Peek-a-boo! https://t.co/R3P6waHxRa",
+  "display_text_range": [
+    0,
+    35
+  ],
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [],
+    "urls": [],
+    "media": [
+      {
+        "id": 706860403746181121,
+        "id_str": "706860403746181121",
+        "indices": [
+          12,
+          35
+        ],
+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "url": "https://t.co/R3P6waHxRa",
+        "display_url": "pic.twitter.com/R3P6waHxRa",
+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",
+        "type": "photo",
+        "sizes": {
+          "medium": {
+            "w": 600,
+            "h": 398,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 226,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 1024,
+            "h": 680,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "extended_entities": {
+    "media": [
+      {
+        "id": 706860403746181121,
+        "id_str": "706860403746181121",
+        "indices": [
+          12,
+          35
+        ],
+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",
+        "url": "https://t.co/R3P6waHxRa",
+        "display_url": "pic.twitter.com/R3P6waHxRa",
+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",
+        "type": "photo",
+        "sizes": {
+          "medium": {
+            "w": 600,
+            "h": 398,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 226,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 1024,
+            "h": 680,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "truncated": false,
+  "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",
+  "in_reply_to_status_id": null,
+  "in_reply_to_status_id_str": null,
+  "in_reply_to_user_id": null,
+  "in_reply_to_user_id_str": null,
+  "in_reply_to_screen_name": null,
+  "user": {
+    "id": 15062340,
+    "id_str": "15062340",
+    "name": "/dev/cloud/jeremy",
+    "screen_name": "jeremycloud",
+    "location": "Madison, Wisconsin",
+    "description": "Professional yak shaver. Amateur bike shedder.",
+    "url": "https://t.co/FcYeBkOpVY",
+    "entities": {
+      "url": {
+        "urls": [
+          {
+            "url": "https://t.co/FcYeBkOpVY",
+            "expanded_url": "http://about.me/jeremy.cloud",
+            "display_url": "about.me/jeremy.cloud",
+            "indices": [
+              0,
+              23
+            ]
+          }
+        ]
+      },
+      "description": {
+        "urls": []
+      }
+    },
+    "protected": false,
+    "followers_count": 4324,
+    "friends_count": 410,
+    "listed_count": 103,
+    "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+    "favourites_count": 815,
+    "utc_offset": -18000,
+    "time_zone": "Central Time (US & Canada)",
+    "geo_enabled": true,
+    "verified": false,
+    "statuses_count": 2218,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "000000",
+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+    "profile_link_color": "4A913C",
+    "profile_sidebar_border_color": "000000",
+    "profile_sidebar_fill_color": "000000",
+    "profile_text_color": "000000",
+    "profile_use_background_image": false,
+    "has_extended_profile": true,
+    "default_profile": false,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "is_quote_status": false,
+  "retweet_count": 0,
+  "favorite_count": 8,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibility_classic_hidden_13797.json b/tests/fixtures/tweet-updates/compatibility_classic_hidden_13797.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibility_classic_hidden_13797.json
@@ -0,0 +1,270 @@
+{
+  "created_at": "Thu Mar 10 23:12:12 +0000 2016",
+  "id": 708067963060916224,
+  "id_str": "708067963060916224",
+  "text": "@jeremycloud Who would win in a battle between a Barred Owl and a Cooper's Hawk? https://t.co/FamikDro2h",
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "jeremycloud",
+        "name": "/dev/cloud/jeremy",
+        "id": 15062340,
+        "id_str": "15062340",
+        "indices": [
+          0,
+          12
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/FamikDro2h",
+        "expanded_url": "https://twitter.com/jeremycloud/status/703621193417379840",
+        "display_url": "twitter.com/jeremycloud/stâ€¦",
+        "indices": [
+          81,
+          104
+        ]
+      }
+    ]
+  },
+  "truncated": false,
+  "source": "bonesTwurl",
+  "in_reply_to_status_id": 704059336788606976,
+  "in_reply_to_status_id_str": "704059336788606976",
+  "in_reply_to_user_id": 15062340,
+  "in_reply_to_user_id_str": "15062340",
+  "in_reply_to_screen_name": "jeremycloud",
+  "user": {
+    "id": 4449621923,
+    "id_str": "4449621923",
+    "name": "Mr Bones",
+    "screen_name": "MrBonesDroid",
+    "location": "",
+    "profile_location": null,
+    "description": "",
+    "url": null,
+    "entities": {
+    "description": {
+    "urls": []
+     }
+    },
+    "protected": true,
+    "followers_count": 5,
+    "friends_count": 7,
+    "listed_count": 0,
+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",
+    "favourites_count": 7,
+    "utc_offset": -25200,
+    "time_zone": "Pacific Time (US & Canada)",
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 35,
+    "lang": "en-gb",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "F5F8FA",
+    "profile_background_image_url": null,
+    "profile_background_image_url_https": null,
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_link_color": "2B7BB9",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 703621193417379840,
+  "quoted_status_id_str": "703621193417379840",
+  "quoted_status": {
+    "created_at": "Sat Feb 27 16:42:19 +0000 2016",
+    "id": 703621193417379840,
+    "id_str": "703621193417379840",
+    "text": "Cooperâ€™s Hawk https://t.co/nppuOGne9X",
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [],
+      "urls": [],
+      "media": [
+        {
+          "id": 703621193182502913,
+          "id_str": "703621193182502913",
+          "indices": [
+            14,
+            37
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "url": "https://t.co/nppuOGne9X",
+          "display_url": "pic.twitter.com/nppuOGne9X",
+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 398,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 1024,
+              "h": 680,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 226,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "extended_entities": {
+      "media": [
+        {
+          "id": 703621193182502913,
+          "id_str": "703621193182502913",
+          "indices": [
+            14,
+            37
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "url": "https://t.co/nppuOGne9X",
+          "display_url": "pic.twitter.com/nppuOGne9X",
+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 398,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 1024,
+              "h": 680,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 226,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "truncated": false,
+    "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user": {
+      "id": 15062340,
+      "id_str": "15062340",
+      "name": "/dev/cloud/jeremy",
+      "screen_name": "jeremycloud",
+      "location": "Madison, Wisconsin",
+      "description": "Professional yak shaver. Amateur bike shedder.",
+      "url": "https://t.co/FcYeBkOpVY",
+      "entities": {
+        "url": {
+          "urls": [
+            {
+              "url": "https://t.co/FcYeBkOpVY",
+              "expanded_url": "http://about.me/jeremy.cloud",
+              "display_url": "about.me/jeremy.cloud",
+              "indices": [
+                0,
+                23
+              ]
+            }
+          ]
+        },
+        "description": {
+          "urls": []
+        }
+      },
+      "protected": false,
+      "followers_count": 4329,
+      "friends_count": 411,
+      "listed_count": 103,
+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+      "favourites_count": 803,
+      "utc_offset": -21600,
+      "time_zone": "Central Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 2216,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "000000",
+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_tile": false,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+      "profile_link_color": "4A913C",
+      "profile_sidebar_border_color": "000000",
+      "profile_sidebar_fill_color": "000000",
+      "profile_text_color": "000000",
+      "profile_use_background_image": false,
+      "has_extended_profile": true,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 0,
+    "favorite_count": 2,
+    "favorited": false,
+    "retweeted": false,
+    "possibly_sensitive": false,
+    "possibly_sensitive_appealable": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibility_extended_13996.json b/tests/fixtures/tweet-updates/compatibility_extended_13996.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibility_extended_13996.json
@@ -0,0 +1,278 @@
+{
+  "created_at": "Thu Mar 10 23:12:12 +0000 2016",
+  "id": 708067963060916224,
+  "id_str": "708067963060916224",
+  "full_text": "@jeremycloud Who would win in a battle between a Barred Owl and a Cooper's Hawk? https://t.co/FamikDro2h",
+  "display_text_range": [
+    13,
+    80
+  ],
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "jeremycloud",
+        "name": "/dev/cloud/jeremy",
+        "id": 15062340,
+        "id_str": "15062340",
+        "indices": [
+          0,
+          12
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/FamikDro2h",
+        "expanded_url": "https://twitter.com/jeremycloud/status/703621193417379840",
+        "display_url": "twitter.com/jeremycloud/stâ€¦",
+        "indices": [
+          81,
+          104
+        ]
+      }
+    ]
+  },
+  "truncated": false,
+  "source": "bonesTwurl",
+  "in_reply_to_status_id": 704059336788606976,
+  "in_reply_to_status_id_str": "704059336788606976",
+  "in_reply_to_user_id": 15062340,
+  "in_reply_to_user_id_str": "15062340",
+  "in_reply_to_screen_name": "jeremycloud",
+  "user": {
+    "id": 4449621923,
+    "id_str": "4449621923",
+    "name": "Mr Bones",
+    "screen_name": "MrBonesDroid",
+    "location": "",
+    "profile_location": null,
+    "description": "",
+    "url": null,
+    "entities": {
+    "description": {
+    "urls": []
+     }
+    },
+    "protected": true,
+    "followers_count": 5,
+    "friends_count": 7,
+    "listed_count": 0,
+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",
+    "favourites_count": 7,
+    "utc_offset": -25200,
+    "time_zone": "Pacific Time (US & Canada)",
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 35,
+    "lang": "en-gb",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "F5F8FA",
+    "profile_background_image_url": null,
+    "profile_background_image_url_https": null,
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_link_color": "2B7BB9",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 703621193417379840,
+  "quoted_status_id_str": "703621193417379840",
+  "quoted_status": {
+    "created_at": "Sat Feb 27 16:42:19 +0000 2016",
+    "id": 703621193417379840,
+    "id_str": "703621193417379840",
+    "full_text": "Cooperâ€™s Hawk https://t.co/nppuOGne9X",
+    "display_text_range": [
+      0,
+      37
+    ],
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [],
+      "urls": [],
+      "media": [
+        {
+          "id": 703621193182502913,
+          "id_str": "703621193182502913",
+          "indices": [
+            14,
+            37
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "url": "https://t.co/nppuOGne9X",
+          "display_url": "pic.twitter.com/nppuOGne9X",
+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 398,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 1024,
+              "h": 680,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 226,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "extended_entities": {
+      "media": [
+        {
+          "id": 703621193182502913,
+          "id_str": "703621193182502913",
+          "indices": [
+            14,
+            37
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",
+          "url": "https://t.co/nppuOGne9X",
+          "display_url": "pic.twitter.com/nppuOGne9X",
+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 398,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 1024,
+              "h": 680,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 226,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "truncated": false,
+    "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user": {
+      "id": 15062340,
+      "id_str": "15062340",
+      "name": "/dev/cloud/jeremy",
+      "screen_name": "jeremycloud",
+      "location": "Madison, Wisconsin",
+      "description": "Professional yak shaver. Amateur bike shedder.",
+      "url": "https://t.co/FcYeBkOpVY",
+      "entities": {
+        "url": {
+          "urls": [
+            {
+              "url": "https://t.co/FcYeBkOpVY",
+              "expanded_url": "http://about.me/jeremy.cloud",
+              "display_url": "about.me/jeremy.cloud",
+              "indices": [
+                0,
+                23
+              ]
+            }
+          ]
+        },
+        "description": {
+          "urls": []
+        }
+      },
+      "protected": false,
+      "followers_count": 4329,
+      "friends_count": 411,
+      "listed_count": 103,
+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+      "favourites_count": 803,
+      "utc_offset": -21600,
+      "time_zone": "Central Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 2216,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "000000",
+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_tile": false,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+      "profile_link_color": "4A913C",
+      "profile_sidebar_border_color": "000000",
+      "profile_sidebar_fill_color": "000000",
+      "profile_text_color": "000000",
+      "profile_use_background_image": false,
+      "has_extended_profile": true,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 0,
+    "favorite_count": 2,
+    "favorited": false,
+    "retweeted": false,
+    "possibly_sensitive": false,
+    "possibly_sensitive_appealable": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibilityplus_classic_13994.json b/tests/fixtures/tweet-updates/compatibilityplus_classic_13994.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibilityplus_classic_13994.json
@@ -0,0 +1,270 @@
+{
+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",
+  "id": 714461850188926976,
+  "id_str": "714461850188926976",
+  "text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the â€¦ https://t.co/OY7qmdJQnO",
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "jeremycloud",
+        "name": "/dev/cloud/jeremy",
+        "id": 15062340,
+        "id_str": "15062340",
+        "indices": [
+          0,
+          12
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/OY7qmdJQnO",
+        "expanded_url": "https://twitter.com/i/web/status/714461850188926976",
+        "display_url": "twitter.com/i/web/status/7â€¦",
+        "indices": [
+          117,
+          140
+        ]
+      }
+    ]
+  },
+  "truncated": true,
+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
+  "in_reply_to_status_id": 706860403981099008,
+  "in_reply_to_status_id_str": "706860403981099008",
+  "in_reply_to_user_id": 15062340,
+  "in_reply_to_user_id_str": "15062340",
+  "in_reply_to_screen_name": "jeremycloud",
+  "user": {
+    "id": 4449621923,
+    "id_str": "4449621923",
+    "name": "Mr Bones",
+    "screen_name": "MrBonesDroid",
+    "location": "",
+    "profile_location": null,
+    "description": "",
+    "url": null,
+    "entities": {
+    "description": {
+    "urls": []
+     }
+    },
+    "protected": true,
+    "followers_count": 5,
+    "friends_count": 7,
+    "listed_count": 0,
+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",
+    "favourites_count": 7,
+    "utc_offset": -25200,
+    "time_zone": "Pacific Time (US & Canada)",
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 35,
+    "lang": "en-gb",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "F5F8FA",
+    "profile_background_image_url": null,
+    "profile_background_image_url_https": null,
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_link_color": "2B7BB9",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 704059336788606976,
+  "quoted_status_id_str": "704059336788606976",
+  "quoted_status": {
+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",
+    "id": 704059336788606976,
+    "id_str": "704059336788606976",
+    "text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [],
+      "urls": [],
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "extended_entities": {
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "truncated": false,
+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user": {
+      "id": 15062340,
+      "id_str": "15062340",
+      "name": "/dev/cloud/jeremy",
+      "screen_name": "jeremycloud",
+      "location": "Madison, Wisconsin",
+      "description": "Professional yak shaver. Amateur bike shedder.",
+      "url": "https://t.co/FcYeBkOpVY",
+      "entities": {
+        "url": {
+          "urls": [
+            {
+              "url": "https://t.co/FcYeBkOpVY",
+              "expanded_url": "http://about.me/jeremy.cloud",
+              "display_url": "about.me/jeremy.cloud",
+              "indices": [
+                0,
+                23
+              ]
+            }
+          ]
+        },
+        "description": {
+          "urls": []
+        }
+      },
+      "protected": false,
+      "followers_count": 4324,
+      "friends_count": 410,
+      "listed_count": 103,
+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+      "favourites_count": 815,
+      "utc_offset": -18000,
+      "time_zone": "Central Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 2218,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "000000",
+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_tile": false,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+      "profile_link_color": "4A913C",
+      "profile_sidebar_border_color": "000000",
+      "profile_sidebar_fill_color": "000000",
+      "profile_text_color": "000000",
+      "profile_use_background_image": false,
+      "has_extended_profile": true,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 0,
+    "favorite_count": 11,
+    "favorited": false,
+    "retweeted": false,
+    "possibly_sensitive": false,
+    "possibly_sensitive_appealable": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibilityplus_classic_hidden_13797.json b/tests/fixtures/tweet-updates/compatibilityplus_classic_hidden_13797.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibilityplus_classic_hidden_13797.json
@@ -0,0 +1,304 @@
+{
+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",
+  "id": 714461850188926976,
+  "id_str": "714461850188926976",
+  "text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the â€¦ https://t.co/OY7qmdJQnO",
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "jeremycloud",
+        "name": "/dev/cloud/jeremy",
+        "id": 15062340,
+        "id_str": "15062340",
+        "indices": [
+          0,
+          12
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/OY7qmdJQnO",
+        "expanded_url": "https://twitter.com/i/web/status/714461850188926976",
+        "display_url": "twitter.com/i/web/status/7â€¦",
+        "indices": [
+          117,
+          140
+        ]
+      }
+    ]
+  },
+  "truncated": true,
+  "extended_tweet": {
+    "full_text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the owl's nest https://t.co/Q0pkaU4ORH",
+    "display_text_range": [
+      13,
+      125
+    ],
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [
+        {
+          "screen_name": "jeremycloud",
+          "name": "/dev/cloud/jeremy",
+          "id": 15062340,
+          "id_str": "15062340",
+          "indices": [
+            0,
+            12
+          ]
+        }
+      ],
+      "urls": [
+        {
+          "url": "https://t.co/Q0pkaU4ORH",
+          "expanded_url": "https://twitter.com/jeremycloud/status/704059336788606976",
+          "display_url": "twitter.com/jeremycloud/stâ€¦",
+          "indices": [
+            126,
+            149
+          ]
+        }
+      ]
+    }
+  },
+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
+  "in_reply_to_status_id": 706860403981099008,
+  "in_reply_to_status_id_str": "706860403981099008",
+  "in_reply_to_user_id": 15062340,
+  "in_reply_to_user_id_str": "15062340",
+  "in_reply_to_screen_name": "jeremycloud",
+  "user": {
+    "id": 4449621923,
+    "id_str": "4449621923",
+    "name": "Mr Bones",
+    "screen_name": "MrBonesDroid",
+    "location": "",
+    "profile_location": null,
+    "description": "",
+    "url": null,
+    "entities": {
+    "description": {
+    "urls": []
+     }
+    },
+    "protected": true,
+    "followers_count": 5,
+    "friends_count": 7,
+    "listed_count": 0,
+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",
+    "favourites_count": 7,
+    "utc_offset": -25200,
+    "time_zone": "Pacific Time (US & Canada)",
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 35,
+    "lang": "en-gb",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "F5F8FA",
+    "profile_background_image_url": null,
+    "profile_background_image_url_https": null,
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_link_color": "2B7BB9",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 704059336788606976,
+  "quoted_status_id_str": "704059336788606976",
+  "quoted_status": {
+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",
+    "id": 704059336788606976,
+    "id_str": "704059336788606976",
+    "text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [],
+      "urls": [],
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "extended_entities": {
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "truncated": false,
+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user": {
+      "id": 15062340,
+      "id_str": "15062340",
+      "name": "/dev/cloud/jeremy",
+      "screen_name": "jeremycloud",
+      "location": "Madison, Wisconsin",
+      "description": "Professional yak shaver. Amateur bike shedder.",
+      "url": "https://t.co/FcYeBkOpVY",
+      "entities": {
+        "url": {
+          "urls": [
+            {
+              "url": "https://t.co/FcYeBkOpVY",
+              "expanded_url": "http://about.me/jeremy.cloud",
+              "display_url": "about.me/jeremy.cloud",
+              "indices": [
+                0,
+                23
+              ]
+            }
+          ]
+        },
+        "description": {
+          "urls": []
+        }
+      },
+      "protected": false,
+      "followers_count": 4324,
+      "friends_count": 410,
+      "listed_count": 103,
+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+      "favourites_count": 815,
+      "utc_offset": -18000,
+      "time_zone": "Central Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 2218,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "000000",
+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_tile": false,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+      "profile_link_color": "4A913C",
+      "profile_sidebar_border_color": "000000",
+      "profile_sidebar_fill_color": "000000",
+      "profile_text_color": "000000",
+      "profile_use_background_image": false,
+      "has_extended_profile": true,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 0,
+    "favorite_count": 11,
+    "favorited": false,
+    "retweeted": false,
+    "possibly_sensitive": false,
+    "possibly_sensitive_appealable": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/compatibilityplus_extended_13997.json b/tests/fixtures/tweet-updates/compatibilityplus_extended_13997.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/compatibilityplus_extended_13997.json
@@ -0,0 +1,278 @@
+{
+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",
+  "id": 714461850188926976,
+  "id_str": "714461850188926976",
+  "full_text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the owl's nest https://t.co/Q0pkaU4ORH",
+  "display_text_range": [
+    13,
+    125
+  ],
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "jeremycloud",
+        "name": "/dev/cloud/jeremy",
+        "id": 15062340,
+        "id_str": "15062340",
+        "indices": [
+          0,
+          12
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/Q0pkaU4ORH",
+        "expanded_url": "https://twitter.com/jeremycloud/status/704059336788606976",
+        "display_url": "twitter.com/jeremycloud/stâ€¦",
+        "indices": [
+          126,
+          149
+        ]
+      }
+    ]
+  },
+  "truncated": false,
+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
+  "in_reply_to_status_id": 706860403981099008,
+  "in_reply_to_status_id_str": "706860403981099008",
+  "in_reply_to_user_id": 15062340,
+  "in_reply_to_user_id_str": "15062340",
+  "in_reply_to_screen_name": "jeremycloud",
+  "user": {
+    "id": 4449621923,
+    "id_str": "4449621923",
+    "name": "Mr Bones",
+    "screen_name": "MrBonesDroid",
+    "location": "",
+    "profile_location": null,
+    "description": "",
+    "url": null,
+    "entities": {
+    "description": {
+    "urls": []
+     }
+    },
+    "protected": true,
+    "followers_count": 5,
+    "friends_count": 7,
+    "listed_count": 0,
+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",
+    "favourites_count": 7,
+    "utc_offset": -25200,
+    "time_zone": "Pacific Time (US & Canada)",
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 35,
+    "lang": "en-gb",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "F5F8FA",
+    "profile_background_image_url": null,
+    "profile_background_image_url_https": null,
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",
+    "profile_link_color": "2B7BB9",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": true,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "quoted_status_id": 704059336788606976,
+  "quoted_status_id_str": "704059336788606976",
+  "quoted_status": {
+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",
+    "id": 704059336788606976,
+    "id_str": "704059336788606976",
+    "full_text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",
+    "display_text_range": [
+      0,
+      85
+    ],
+    "entities": {
+      "hashtags": [],
+      "symbols": [],
+      "user_mentions": [],
+      "urls": [],
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "extended_entities": {
+      "media": [
+        {
+          "id": 704059330149031936,
+          "id_str": "704059330149031936",
+          "indices": [
+            62,
+            85
+          ],
+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",
+          "url": "https://t.co/K958bKh9Sd",
+          "display_url": "pic.twitter.com/K958bKh9Sd",
+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",
+          "type": "photo",
+          "sizes": {
+            "medium": {
+              "w": 600,
+              "h": 600,
+              "resize": "fit"
+            },
+            "thumb": {
+              "w": 150,
+              "h": 150,
+              "resize": "crop"
+            },
+            "large": {
+              "w": 871,
+              "h": 871,
+              "resize": "fit"
+            },
+            "small": {
+              "w": 340,
+              "h": 340,
+              "resize": "fit"
+            }
+          }
+        }
+      ]
+    },
+    "truncated": false,
+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
+    "in_reply_to_status_id": null,
+    "in_reply_to_status_id_str": null,
+    "in_reply_to_user_id": null,
+    "in_reply_to_user_id_str": null,
+    "in_reply_to_screen_name": null,
+    "user": {
+      "id": 15062340,
+      "id_str": "15062340",
+      "name": "/dev/cloud/jeremy",
+      "screen_name": "jeremycloud",
+      "location": "Madison, Wisconsin",
+      "description": "Professional yak shaver. Amateur bike shedder.",
+      "url": "https://t.co/FcYeBkOpVY",
+      "entities": {
+        "url": {
+          "urls": [
+            {
+              "url": "https://t.co/FcYeBkOpVY",
+              "expanded_url": "http://about.me/jeremy.cloud",
+              "display_url": "about.me/jeremy.cloud",
+              "indices": [
+                0,
+                23
+              ]
+            }
+          ]
+        },
+        "description": {
+          "urls": []
+        }
+      },
+      "protected": false,
+      "followers_count": 4324,
+      "friends_count": 410,
+      "listed_count": 103,
+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",
+      "favourites_count": 815,
+      "utc_offset": -18000,
+      "time_zone": "Central Time (US & Canada)",
+      "geo_enabled": true,
+      "verified": false,
+      "statuses_count": 2218,
+      "lang": "en",
+      "contributors_enabled": false,
+      "is_translator": false,
+      "is_translation_enabled": false,
+      "profile_background_color": "000000",
+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+      "profile_background_tile": false,
+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",
+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",
+      "profile_link_color": "4A913C",
+      "profile_sidebar_border_color": "000000",
+      "profile_sidebar_fill_color": "000000",
+      "profile_text_color": "000000",
+      "profile_use_background_image": false,
+      "has_extended_profile": true,
+      "default_profile": false,
+      "default_profile_image": false,
+      "following": true,
+      "follow_request_sent": false,
+      "notifications": false
+    },
+    "geo": null,
+    "coordinates": null,
+    "place": null,
+    "contributors": null,
+    "is_quote_status": false,
+    "retweet_count": 0,
+    "favorite_count": 11,
+    "favorited": false,
+    "retweeted": false,
+    "possibly_sensitive": false,
+    "possibly_sensitive_appealable": false,
+    "lang": "en"
+  },
+  "is_quote_status": true,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/extended_classic_14002.json b/tests/fixtures/tweet-updates/extended_classic_14002.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/extended_classic_14002.json
@@ -0,0 +1,87 @@
+{
+  "created_at": "Thu Jun 16 15:57:14 +0000 2016",
+  "id": 743472511740870657,
+  "id_str": "743472511740870657",
+  "full_text": "Just setting up my Twitter. #myfirstTweet",
+  "truncated": false,
+  "display_text_range": [
+    0,
+    41
+  ],
+  "entities": {
+    "hashtags": [
+      {
+        "text": "myfirstTweet",
+        "indices": [
+          28,
+          41
+        ]
+      }
+    ],
+    "symbols": [],
+    "user_mentions": [],
+    "urls": []
+  },
+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",
+  "in_reply_to_status_id": null,
+  "in_reply_to_status_id_str": null,
+  "in_reply_to_user_id": null,
+  "in_reply_to_user_id_str": null,
+  "in_reply_to_screen_name": null,
+  "user": {
+    "id": 3883872981,
+    "id_str": "3883872981",
+    "name": "Beyond 140",
+    "screen_name": "beyond_oneforty",
+    "location": "",
+    "description": "",
+    "url": null,
+    "entities": {
+      "description": {
+        "urls": []
+      }
+    },
+    "protected": true,
+    "followers_count": 51,
+    "friends_count": 0,
+    "listed_count": 0,
+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",
+    "favourites_count": 1,
+    "utc_offset": null,
+    "time_zone": null,
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 9,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "C0DEED",
+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",
+    "profile_link_color": "0084B4",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": false,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "is_quote_status": false,
+  "retweet_count": 0,
+  "favorite_count": 1,
+  "favorited": false,
+  "retweeted": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/extended_classic_hidden_13761.json b/tests/fixtures/tweet-updates/extended_classic_hidden_13761.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/extended_classic_hidden_13761.json
@@ -0,0 +1,183 @@
+{
+  "created_at": "Thu Jun 16 16:24:44 +0000 2016",
+  "id": 743479431658758145,
+  "id_str": "743479431658758145",
+  "full_text": "@TwitterDev I'm really excited for this change! https://t.co/VnJMDg3cbS",
+  "truncated": false,
+  "display_text_range": [
+    12,
+    47
+  ],
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "TwitterDev",
+        "name": "TwitterDev",
+        "id": 2244994945,
+        "id_str": "2244994945",
+        "indices": [
+          0,
+          11
+        ]
+      }
+    ],
+    "urls": [],
+    "media": [
+      {
+        "id": 743479379079004160,
+        "id_str": "743479379079004160",
+        "indices": [
+          48,
+          71
+        ],
+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",
+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",
+        "url": "https://t.co/VnJMDg3cbS",
+        "display_url": "pic.twitter.com/VnJMDg3cbS",
+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1",
+        "type": "photo",
+        "sizes": {
+          "medium": {
+            "w": 480,
+            "h": 268,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 480,
+            "h": 268,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 190,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "extended_entities": {
+    "media": [
+      {
+        "id": 743479379079004160,
+        "id_str": "743479379079004160",
+        "indices": [
+          48,
+          71
+        ],
+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",
+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",
+        "url": "https://t.co/VnJMDg3cbS",
+        "display_url": "pic.twitter.com/VnJMDg3cbS",
+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1",
+        "type": "animated_gif",
+        "sizes": {
+          "medium": {
+            "w": 480,
+            "h": 268,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 480,
+            "h": 268,
+            "resize": "fit"
+          },
+          "small": {
+            "w": 340,
+            "h": 190,
+            "resize": "fit"
+          }
+        },
+        "video_info": {
+          "aspect_ratio": [
+            120,
+            67
+          ],
+          "variants": [
+            {
+              "bitrate": 0,
+              "content_type": "video/mp4",
+              "url": "https://pbs.twimg.com/tweet_video/ClFejl_VAAAo9Xk.mp4"
+            }
+          ]
+        }
+      }
+    ]
+  },
+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",
+  "in_reply_to_status_id": 735111578660438017,
+  "in_reply_to_status_id_str": "735111578660438017",
+  "in_reply_to_user_id": 2244994945,
+  "in_reply_to_user_id_str": "2244994945",
+  "in_reply_to_screen_name": "TwitterDev",
+  "user": {
+    "id": 3883872981,
+    "id_str": "3883872981",
+    "name": "Beyond 140",
+    "screen_name": "beyond_oneforty",
+    "location": "",
+    "description": "",
+    "url": null,
+    "entities": {
+      "description": {
+        "urls": []
+      }
+    },
+    "protected": true,
+    "followers_count": 51,
+    "friends_count": 0,
+    "listed_count": 0,
+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",
+    "favourites_count": 1,
+    "utc_offset": null,
+    "time_zone": null,
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 10,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "C0DEED",
+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",
+    "profile_link_color": "0084B4",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": false,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "is_quote_status": false,
+  "retweet_count": 0,
+  "favorite_count": 1,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/tweet-updates/extended_extended_14001.json b/tests/fixtures/tweet-updates/extended_extended_14001.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/tweet-updates/extended_extended_14001.json
@@ -0,0 +1,213 @@
+{
+  "created_at": "Thu Jun 16 17:33:23 +0000 2016",
+  "id": 743496707711733760,
+  "id_str": "743496707711733760",
+  "full_text": "@twitter @twitterdev has more details about these changes at https://t.co/ZnXoRQy8mK.  Thanks for making @twitter more expressive! https://t.co/AWmiH870F7",
+  "truncated": false,
+  "display_text_range": [
+    9,
+    130
+  ],
+  "entities": {
+    "hashtags": [],
+    "symbols": [],
+    "user_mentions": [
+      {
+        "screen_name": "twitter",
+        "name": "Twitter",
+        "id": 783214,
+        "id_str": "783214",
+        "indices": [
+          0,
+          8
+        ]
+      },
+      {
+        "screen_name": "TwitterDev",
+        "name": "TwitterDev",
+        "id": 2244994945,
+        "id_str": "2244994945",
+        "indices": [
+          9,
+          20
+        ]
+      },
+      {
+        "screen_name": "twitter",
+        "name": "Twitter",
+        "id": 783214,
+        "id_str": "783214",
+        "indices": [
+          105,
+          113
+        ]
+      }
+    ],
+    "urls": [
+      {
+        "url": "https://t.co/ZnXoRQy8mK",
+        "expanded_url": "https://blog.twitter.com/2016/doing-more-with-140-characters",
+        "display_url": "blog.twitter.com/2016/doing-mor…",
+        "indices": [
+          61,
+          84
+        ]
+      }
+    ],
+    "media": [
+      {
+        "id": 743495942192566272,
+        "id_str": "743495942192566272",
+        "indices": [
+          131,
+          154
+        ],
+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",
+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",
+        "url": "https://t.co/AWmiH870F7",
+        "display_url": "pic.twitter.com/AWmiH870F7",
+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743496707711733760/photo/1",
+        "type": "photo",
+        "sizes": {
+          "small": {
+            "w": 340,
+            "h": 255,
+            "resize": "fit"
+          },
+          "medium": {
+            "w": 480,
+            "h": 360,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 480,
+            "h": 360,
+            "resize": "fit"
+          }
+        }
+      }
+    ]
+  },
+  "extended_entities": {
+    "media": [
+      {
+        "id": 743495942192566272,
+        "id_str": "743495942192566272",
+        "indices": [
+          131,
+          154
+        ],
+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",
+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",
+        "url": "https://t.co/AWmiH870F7",
+        "display_url": "pic.twitter.com/AWmiH870F7",
+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743496707711733760/photo/1",
+        "type": "animated_gif",
+        "sizes": {
+          "small": {
+            "w": 340,
+            "h": 255,
+            "resize": "fit"
+          },
+          "medium": {
+            "w": 480,
+            "h": 360,
+            "resize": "fit"
+          },
+          "thumb": {
+            "w": 150,
+            "h": 150,
+            "resize": "crop"
+          },
+          "large": {
+            "w": 480,
+            "h": 360,
+            "resize": "fit"
+          }
+        },
+        "video_info": {
+          "aspect_ratio": [
+            4,
+            3
+          ],
+          "variants": [
+            {
+              "bitrate": 0,
+              "content_type": "video/mp4",
+              "url": "https://pbs.twimg.com/tweet_video/ClFtnsZVEAAE4oA.mp4"
+            }
+          ]
+        }
+      }
+    ]
+  },
+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",
+  "in_reply_to_status_id": 735108260718469121,
+  "in_reply_to_status_id_str": "735108260718469121",
+  "in_reply_to_user_id": 783214,
+  "in_reply_to_user_id_str": "783214",
+  "in_reply_to_screen_name": "twitter",
+  "user": {
+    "id": 3883872981,
+    "id_str": "3883872981",
+    "name": "Beyond 140",
+    "screen_name": "beyond_oneforty",
+    "location": "",
+    "description": "",
+    "url": null,
+    "entities": {
+      "description": {
+        "urls": []
+      }
+    },
+    "protected": true,
+    "followers_count": 51,
+    "friends_count": 2,
+    "listed_count": 0,
+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",
+    "favourites_count": 1,
+    "utc_offset": null,
+    "time_zone": null,
+    "geo_enabled": false,
+    "verified": false,
+    "statuses_count": 11,
+    "lang": "en",
+    "contributors_enabled": false,
+    "is_translator": false,
+    "is_translation_enabled": false,
+    "profile_background_color": "C0DEED",
+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
+    "profile_background_tile": false,
+    "profile_image_url": "http://pbs.twimg.com/profile_images/743489433861685249/VOcKbH8Z_normal.jpg",
+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/743489433861685249/VOcKbH8Z_normal.jpg",
+    "profile_link_color": "0084B4",
+    "profile_sidebar_border_color": "C0DEED",
+    "profile_sidebar_fill_color": "DDEEF6",
+    "profile_text_color": "333333",
+    "profile_use_background_image": true,
+    "has_extended_profile": false,
+    "default_profile": true,
+    "default_profile_image": false,
+    "following": false,
+    "follow_request_sent": false,
+    "notifications": false
+  },
+  "geo": null,
+  "coordinates": null,
+  "place": null,
+  "contributors": null,
+  "is_quote_status": false,
+  "retweet_count": 0,
+  "favorite_count": 0,
+  "favorited": false,
+  "retweeted": false,
+  "possibly_sensitive": false,
+  "possibly_sensitive_appealable": false,
+  "lang": "en"
+}
diff --git a/tests/fixtures/user_thimura.json b/tests/fixtures/user_thimura.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/user_thimura.json
@@ -0,0 +1,1 @@
+{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","status":{"in_reply_to_status_id":null,"id_str":"495192122836783104","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"くそあつい","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"lang":"ja","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Fri Aug 01 12:59:36 +0000 2014","source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495192122836783104,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null},"is_translation_enabled":false,"needs_phone_verification":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","suspended":false,"verified":false,"statuses_count":24709,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":780,"is_translator":false,"favourites_count":17313,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":754}
diff --git a/tests/fixtures/user_thimura_lang_null.json b/tests/fixtures/user_thimura_lang_null.json
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/user_thimura_lang_null.json
@@ -0,0 +1,1 @@
+{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","status":{"in_reply_to_status_id":null,"id_str":"495192122836783104","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"くそあつい","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"lang":null,"retweet_count":0,"in_reply_to_user_id":null,"created_at":"Fri Aug 01 12:59:36 +0000 2014","source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495192122836783104,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null},"is_translation_enabled":false,"needs_phone_verification":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","suspended":false,"verified":false,"statuses_count":24709,"profile_background_tile":true,"following":false,"lang":null,"follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":780,"is_translator":false,"favourites_count":17313,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":754}
diff --git a/tests/spec_main.hs b/tests/spec_main.hs
new file mode 100644
--- /dev/null
+++ b/tests/spec_main.hs
@@ -0,0 +1,17 @@
+module Main where
+
+import qualified PropFromToJSONTest
+import qualified StatusTest
+import Test.Tasty
+import qualified TypesTest
+
+tests :: TestTree
+tests =
+    testGroup
+        "Tests"
+        [ testGroup "Unit Test" [TypesTest.tests, StatusTest.tests]
+        , testGroup "Property Test" [PropFromToJSONTest.tests]
+        ]
+
+main :: IO ()
+main = defaultMain tests
diff --git a/twitter-types.cabal b/twitter-types.cabal
--- a/twitter-types.cabal
+++ b/twitter-types.cabal
@@ -1,57 +1,65 @@
-name:              twitter-types
-version:           0.3.20140801
-license:           BSD3
-license-file:      LICENSE
-author:            Takahiro HIMURA
-maintainer:        Takahiro HIMURA <taka@himura.jp>
-synopsis:          Twitter JSON parser and types
-description:       This package uses enumerator package for access Twitter API.
-category:          Web, Enumerator
-stability:         Experimental
-cabal-version:     >= 1.8
-build-type:        Simple
-homepage:          https://github.com/himura/twitter-types
+cabal-version:      >=1.10
+name:               twitter-types
+version:            0.11.0
+license:            BSD3
+license-file:       LICENSE
+maintainer:         Takahiro HIMURA <taka@himura.jp>
+author:             Takahiro HIMURA
+stability:          Experimental
+tested-with:        ghc ==8.8.4 ghc ==8.10.4 ghc ==9.0.1
+homepage:           https://github.com/himura/twitter-types
+synopsis:           Twitter JSON parser and types
+description:
+    Please see the README on Github at <https://github.com/himura/twitter-types#readme>
 
+category:           Web
+build-type:         Simple
 extra-source-files:
-  .gitignore
-  .travis.yml
+    README.md
+    tests/fixtures/*.json
+    tests/fixtures/tweet-updates/*.json
 
 source-repository head
-  type: git
-  location: git://github.com/himura/twitter-types.git
+    type:     git
+    location: git://github.com/himura/twitter-types.git
 
 library
-  ghc-options: -Wall
-
-  build-depends:
-      base >= 4 && < 5
-    , http-types
-    , aeson >= 0.3.2.2
-    , text
-    , unordered-containers
-
-  exposed-modules:
-    Web.Twitter.Types
-    Web.Twitter.Types.Lens
+    exposed-modules:  Web.Twitter.Types
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=4.9 && <5,
+        aeson >=0.3.2.2,
+        text,
+        time >=1.5,
+        unordered-containers
 
 test-suite tests
-  type:              exitcode-stdio-1.0
-  hs-source-dirs:    tests, .
-  main-is:           TypesTest.hs
-  build-depends:
-      base >= 4.0 && < 5
-    , template-haskell
-    , test-framework >= 0.3.3
-    , test-framework-th-prime
-    , test-framework-hunit
-    , HUnit
-    , shakespeare >= 2.0
-    , http-types
-    , aeson
-    , attoparsec
-    , text
-    , unordered-containers
-  other-modules:
-    Fixtures
+    type:             exitcode-stdio-1.0
+    main-is:          spec_main.hs
+    hs-source-dirs:   tests
+    other-modules:
+        Fixtures
+        Instances
+        PropFromToJSONTest
+        StatusTest
+        TypesTest
 
-  ghc-options:       -Wall
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=4.9 && <5,
+        twitter-types,
+        aeson,
+        attoparsec,
+        bytestring,
+        generic-random,
+        directory,
+        filepath,
+        tasty >=0.7,
+        tasty-hunit,
+        tasty-quickcheck,
+        tasty-th,
+        text,
+        time,
+        unordered-containers
