diff --git a/aeson-schema.cabal b/aeson-schema.cabal
--- a/aeson-schema.cabal
+++ b/aeson-schema.cabal
@@ -1,5 +1,5 @@
 name:                aeson-schema
-version:             0.3.0.1
+version:             0.3.0.2
 synopsis:            Haskell JSON schema validator and parser generator
 -- description:         
 homepage:            https://github.com/timjb/aeson-schema
@@ -10,6 +10,8 @@
 category:            Data
 build-type:          Simple
 cabal-version:       >=1.8
+data-files:          test/test-suite/tests/draft3/optional/*.json,
+                     test/test-suite/tests/draft3/*.json
 
 source-repository head
   type:                git
@@ -52,6 +54,13 @@
   hs-source-dirs:      test
   type:                exitcode-stdio-1.0
   main-is:             TestSuite.hs
+  other-modules:       TestSuite.Types,
+                       Data.Aeson.LitQQ,
+                       Data.Aeson.Schema.Examples,
+                       Data.Aeson.Schema.Choice.Tests,
+                       Data.Aeson.Schema.CodeGen.Tests,
+                       Data.Aeson.Schema.Types.Tests,
+                       Data.Aeson.Schema.Validator.Tests
   extensions:          OverloadedStrings
   build-depends:       base,
                        aeson,
diff --git a/test/Data/Aeson/LitQQ.hs b/test/Data/Aeson/LitQQ.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/LitQQ.hs
@@ -0,0 +1,24 @@
+{-# OPTIONS_GHC -fno-warn-missing-fields #-}
+
+module Data.Aeson.LitQQ
+  ( aesonLitQQ
+  ) where
+
+import           Control.Applicative              ((*>), (<*))
+import           Data.Aeson.Parser                (value')
+import           Data.Attoparsec.ByteString.Char8 (skipSpace)
+import           Data.Attoparsec.Lazy             (Result (..), parse)
+import           Data.ByteString.Lazy.Char8       (pack)
+import           Language.Haskell.TH              (ExpQ)
+import           Language.Haskell.TH.Quote        (QuasiQuoter (..))
+import           Language.Haskell.TH.Syntax       (lift)
+
+import           Data.Aeson.Schema.CodeGen
+
+aesonLitQQ :: QuasiQuoter
+aesonLitQQ = QuasiQuoter { quoteExp = aesonLit }
+
+aesonLit :: String -> ExpQ
+aesonLit jsonStr = case parse (skipSpace *> value' <* skipSpace) (pack jsonStr) of
+  Done _ json -> lift json
+  _           -> fail "not a valid JSON value"
diff --git a/test/Data/Aeson/Schema/Choice/Tests.hs b/test/Data/Aeson/Schema/Choice/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/Schema/Choice/Tests.hs
@@ -0,0 +1,62 @@
+{-# OPTIONS_GHC -fno-warn-unused-binds #-}
+
+module Data.Aeson.Schema.Choice.Tests
+  ( tests
+  ) where
+
+import           Test.Framework
+import           Test.Framework.Providers.HUnit
+import           Test.Framework.Providers.QuickCheck2
+import qualified Test.HUnit                           as HU
+import           Test.QuickCheck                      (Gen, arbitrary, vectorOf)
+
+import           Data.Aeson
+import           Data.Char                            (toUpper)
+import           Data.List                            (nub)
+import           Data.Vector                          (fromList)
+
+import           Data.Aeson.Schema.Choice
+
+choiceShow3 :: Choice3 String Int Bool -> String
+choiceShow3 (Choice1of3 s) = s
+choiceShow3 (Choice2of3 i) = show i
+choiceShow3 (Choice3of3 b) = if b then "yes" else "no"
+
+choiceEqual3 :: (Eq a, Eq b, Eq c) => Choice3 a b c -> Choice3 a b c -> Bool
+choiceEqual3 = (==)
+
+choiceOrd3 :: (Ord a, Ord b, Ord c) => Choice3 a b c -> Choice3 a b c -> Ordering
+choiceOrd3 = compare
+
+choiceShow7 :: (Show a, Show b, Show c, Show d) => Choice7 String Int Bool a b c d -> String
+choiceShow7 (Choice1of7 s) = s
+choiceShow7 (Choice2of7 i) = show i
+choiceShow7 (Choice3of7 b) = if b then "yes" else "no"
+choiceShow7 c = show c
+
+tests :: [Test]
+tests =
+  [ testCase "parseJSON" $ do
+      Success (Choice1of3 3 :: Choice3 Int String [Bool]) HU.@=? fromJSON (Number 3)
+      Success (Choice2of3 "lorem" :: Choice3 Int String [Bool]) HU.@=? fromJSON (String "lorem")
+      Success (Choice3of3 [True,False,True] :: Choice3 Int String [Bool]) HU.@=? fromJSON (Array . fromList . map Bool $ [True,False,True])
+  , testCase "toJSON" $ do
+      object [ "Left" .= String "a" ] HU.@=? toJSON (Choice1of3 (Left 'a') :: Choice3 (Either Char Bool) () (Maybe String))
+      Array (fromList []) HU.@=? toJSON (Choice2of3 () :: Choice3 (Either Char Bool) () (Maybe String))
+      Null HU.@=? toJSON (Choice3of3 Nothing :: Choice3 (Either Char Bool) () (Maybe String))
+  , testProperty "arbitrary" $ do
+      xs <- vectorOf 100 (arbitrary :: Gen (Choice3 () () ()))
+      return $ length (nub xs) == 3
+  , testCase "choice3" $ do
+      let rnd = round :: Double -> Int
+      (3 :: Int) HU.@=? choice3 length id rnd (Choice1of3 ("abc" :: String))
+      (4 :: Int) HU.@=? choice3 length id rnd (Choice2of3 4)
+      (5 :: Int) HU.@=? choice3 length id rnd (Choice3of3 (4.6 :: Double))
+  , testCase "mapChoice3" $ do
+      let plus1 = (+1) :: Int -> Int
+      Choice1of3 "LOREM" HU.@=? mapChoice3 (map toUpper) plus1 not (Choice1of3 "lorem")
+      Choice2of3 4 HU.@=? mapChoice3 (map toUpper) plus1 not (Choice2of3 (3 :: Int))
+      Choice3of3 True HU.@=? mapChoice3 (map toUpper) plus1 not (Choice3of3 False)
+  , testCase "choice2of3s" $ do
+      ["eins" :: String, "zwei", "drei"] HU.@=? choice2of3s [Choice1of3 True, Choice2of3 "eins", Choice2of3 "zwei", Choice3of3 '?', Choice2of3 "drei"]
+  ]
diff --git a/test/Data/Aeson/Schema/CodeGen/Tests.hs b/test/Data/Aeson/Schema/CodeGen/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/Schema/CodeGen/Tests.hs
@@ -0,0 +1,359 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleInstances         #-}
+{-# LANGUAGE ImpredicativeTypes        #-}
+{-# LANGUAGE QuasiQuotes               #-}
+{-# LANGUAGE RankNTypes                #-}
+{-# LANGUAGE ScopedTypeVariables       #-}
+{-# LANGUAGE TupleSections             #-}
+
+module Data.Aeson.Schema.CodeGen.Tests
+  ( tests
+  ) where
+
+import           Test.Framework
+import           Test.Framework.Providers.HUnit
+import           Test.Framework.Providers.QuickCheck2
+import qualified Test.HUnit                           as HU
+import           Test.QuickCheck                      hiding (Result (..))
+import           Test.QuickCheck.Property             (Result (..), failed,
+                                                       ioProperty, succeeded)
+
+import           Control.Applicative                  (pure, (<$>), (<*>))
+import           Control.Concurrent                   (forkIO)
+import           Control.Concurrent.Chan              (Chan, newChan, readChan,
+                                                       writeChan)
+import           Control.Concurrent.MVar              (newEmptyMVar, putMVar,
+                                                       takeMVar)
+import           Control.Monad                        (forever, liftM2, (>=>))
+import           Control.Monad.Trans                  (liftIO)
+import           Data.Aeson                           (Value (..))
+import           Data.Char                            (isAscii, isPrint)
+import           Data.Monoid
+import           Data.Hashable                        (Hashable)
+import qualified Data.HashMap.Lazy                    as HM
+import qualified Data.Map                             as M
+import           Data.Maybe                           (isNothing, listToMaybe)
+import           Data.Scientific                      (Scientific)
+import           Data.Text                            (Text, pack, unpack)
+import qualified Data.Text                            as T
+import qualified Data.Text.IO                         as TIO
+import qualified Data.Vector                          as V
+import qualified Language.Haskell.Interpreter         as Hint
+import qualified Language.Haskell.Interpreter.Unsafe  as Hint
+import           Language.Haskell.TH                  (runQ)
+import           Language.Haskell.TH.Ppr              (pprint)
+import qualified Language.Haskell.TH.Syntax           as THS
+import           System.IO                            (hClose)
+import           System.IO.Temp                       (withSystemTempFile)
+
+import           Data.Aeson.Schema
+import           Data.Aeson.Schema.Choice
+import           Data.Aeson.Schema.CodeGen            (generateModule)
+import           Data.Aeson.Schema.Helpers            (formatValidators,
+                                                       getUsedModules,
+                                                       replaceHiddenModules)
+
+import           Data.Aeson.Schema.Examples           (examples)
+import           TestSuite.Types                      (SchemaTest (..),
+                                                       SchemaTestCase (..))
+import qualified System.Directory                     as DIR
+import qualified System.FilePath                      as FP
+
+instance Arbitrary Text where
+  arbitrary = pack <$> arbitrary
+
+instance (Eq k, Hashable k, Arbitrary k, Arbitrary v) => Arbitrary (HM.HashMap k v) where
+  arbitrary = HM.fromList <$> arbitrary
+
+instance Arbitrary Scientific where
+  arbitrary = fromRational <$> arbitrary
+
+instance (Arbitrary a) => Arbitrary (V.Vector a) where
+  arbitrary = V.fromList <$> arbitrary
+
+instance Arbitrary Pattern where
+  -- TODO: improve performance
+  arbitrary = do
+    arbitraryString <- arbitrary
+    case mkPattern arbitraryString of
+      Just p -> return p
+      Nothing -> arbitrary
+
+arbitraryValue :: Int -> Gen Value
+arbitraryValue 0 = return Null
+arbitraryValue i = oneof
+  [ Object . HM.fromList <$> shortListOf ((,) <$> arbitrary <*> subValue)
+  , Array  . V.fromList  <$> shortListOf subValue
+  , String <$> arbitrary
+  , Number <$> arbitrary
+  , Bool   <$> arbitrary
+  , pure Null
+  ]
+  where
+    subValue = arbitraryValue (i-1)
+    shortListOf = fmap (take 3) . listOf
+
+
+instance Arbitrary Value where
+  arbitrary = arbitraryValue 3
+
+instance Arbitrary SchemaType where
+  arbitrary = elements [minBound..maxBound]
+
+arbitrarySchema :: (Eq a) => Int -> Gen (Schema a)
+arbitrarySchema 0 = return empty
+arbitrarySchema depth = do
+  typ <- shortListOf1 (choice2of arbitrary subSchema)
+  required <- arbitrary
+  disallow <- rareShortListOf (choice2of arbitrary subSchema)
+  extends <- rareShortListOf subSchema
+  description <- arbitrary
+  let sch0 = empty
+        { schemaType = typ
+        , schemaRequired = required
+        , schemaDisallow = disallow
+        , schemaExtends = extends
+        , schemaDescription = description
+        }
+  sch1 <- flip (foldl (>=>) return) sch0
+    [ modifyIf (Choice1of2 ArrayType `elem` typ) $ \sch -> do
+        items <- maybeOf (choice2of subSchema $ shortListOf1 subSchema)
+        additionalItems <- choice2of arbitrary subSchema
+        minItems <- abs <$> arbitrary
+        maxItems <- fmap ((+ minItems) . abs) <$> arbitrary
+        uniqueItems <- arbitrary
+        return $ sch
+          { schemaItems = items
+          , schemaAdditionalItems = additionalItems
+          , schemaMinItems = minItems
+          , schemaMaxItems = maxItems
+          , schemaUniqueItems = uniqueItems
+          }
+    , modifyIf (Choice1of2 ObjectType `elem` typ) $ \sch -> do
+        properties <- smallMapOf (T.filter (\c -> isPrint c && isAscii c) <$> arbitrary) subSchema
+        patternProperties <- shortListOf (tupleOf arbitrary subSchema)
+        additionalProperties <- choice2of arbitrary subSchema
+        dependencies <- smallMapOf arbitrary (choice2of arbitrary subSchema)
+        return $ sch
+          { schemaProperties = properties
+          , schemaPatternProperties = patternProperties
+          , schemaAdditionalProperties = additionalProperties
+          , schemaDependencies = dependencies
+          }
+    , modifyIf (Choice1of2 StringType `elem` typ) $ \sch -> do
+        minLength <- abs <$> arbitrary
+        maxLength <- fmap ((+ minLength) . abs) <$> arbitrary
+        pattern <- arbitrary
+        format <- maybeOf (oneof formats)
+        return $ sch
+          { schemaMinLength = minLength
+          , schemaMaxLength = maxLength
+          , schemaPattern = pattern
+          , schemaFormat = format
+          }
+    , modifyIf (Choice1of2 NumberType `elem` typ || Choice1of2 IntegerType `elem` typ) $ \sch -> do
+        sMinimum <- arbitrary
+        sMaximum <- case sMinimum of
+          Nothing -> arbitrary
+          Just mini -> fmap ((+ mini) . abs) <$> arbitrary
+        exclusiveMinimum <- if isNothing sMinimum then return False else arbitrary
+        exclusiveMaximum <- if isNothing sMaximum then return False else arbitrary
+        divisibleBy <- arbitrary
+        return $ sch
+          { schemaMinimum = sMinimum
+          , schemaMaximum = sMaximum
+          , schemaExclusiveMinimum = exclusiveMinimum
+          , schemaExclusiveMaximum = exclusiveMaximum
+          , schemaDivisibleBy = divisibleBy
+          }
+
+    ]
+  -- the enum and default values are probibly not compatible with the schema
+  -- but that's irrelevant since we're only interested if the generated code
+  -- typechecks
+  enum <- maybeOf (shortListOf arbitrary)
+  dflt <- maybeOf $ case enum of
+    Just vs@(_:_) -> elements vs
+    _             -> arbitrary
+  return $ sch1 { schemaEnum = enum, schemaDefault = dflt }
+  where
+    modifyIf :: (Monad m) => Bool -> (a -> m a) -> a -> m a
+    modifyIf False _ = return
+    modifyIf True  a = a
+    subSchema = arbitrarySchema (depth-1)
+    formats = map (pure . fst) formatValidators
+    maybeOf = fmap listToMaybe . listOf
+    shortListOf = fmap (take 2) . listOf
+    rareShortListOf a = frequency [(2, return []), (1, shortListOf a)]
+    shortListOf1 = fmap (take 2) . listOf1
+    choice2of a b = oneof [Choice1of2 <$> a, Choice2of2 <$> b]
+    tupleOf = liftM2 (,)
+    smallMapOf k v = HM.fromList <$> shortListOf (tupleOf k v)
+
+instance (Eq a) => Arbitrary (Schema a) where
+  arbitrary = arbitrarySchema 3
+
+data ForkLift = ForkLift (Chan (Hint.Interpreter (), Hint.InterpreterError -> IO ()))
+
+getSandboxPackageDB :: IO (Maybe FilePath) 
+getSandboxPackageDB = do
+    cwd <- DIR.getCurrentDirectory
+    contents <- readFile $ cwd <> [FP.pathSeparator] <> "cabal.sandbox.config"
+    return $ findPackageDB contents
+    where
+        packagedb = "package-db: "
+        maybePackageDB :: [FilePath] -> Maybe FilePath
+        maybePackageDB list = case list of 
+                                   [path] -> Just $ T.unpack $ T.replace packagedb "" (T.pack path)
+                                   _ -> Nothing
+        findPackageDB contents = maybePackageDB $ filter (\l -> T.isPrefixOf packagedb (T.pack l)) (lines contents)
+
+getInterpreterArgs :: IO [String]
+getInterpreterArgs = do
+    mdb <- getSandboxPackageDB
+    return $ case mdb of
+                Just path -> ["-no-user-package-db", "-package-db " <> path]  
+                Nothing -> []
+
+-- | uses the Forklift pattern (http://apfelmus.nfshost.com/blog/2012/06/07-forklift.html)
+-- to send commands to an interpreter running in a different thread
+startInterpreterThread :: IO ForkLift
+startInterpreterThread = do
+  cmdChan <- newChan
+  _ <- forkIO $ do
+    errorHandler <- newEmptyMVar
+    args <- getInterpreterArgs 
+
+    forever $ do
+      Left err <- Hint.unsafeRunInterpreterWithArgs args $ do
+        forever $ do
+            (action, handler) <- liftIO $ readChan cmdChan
+            liftIO $ putMVar errorHandler handler
+            action
+            liftIO $ takeMVar errorHandler
+      handler <- takeMVar errorHandler
+      handler err
+  return $ ForkLift cmdChan
+
+carry :: ForkLift -> Hint.Interpreter a -> IO (Either Hint.InterpreterError a)
+carry (ForkLift cmdChan) action = do
+  result <- newEmptyMVar
+  let successHandler = action >>= liftIO . putMVar result . Right
+      errorHandler   = putMVar result . Left
+  writeChan cmdChan (successHandler, errorHandler)
+  takeMVar result
+
+tests :: [SchemaTest] -> IO [Test]
+tests schemaTests = do
+  forkLift <- startInterpreterThread
+  return
+    [ testProperty "generated code typechecks" $ typecheckGenerate forkLift
+    , testGroup "examples" $ testExamples forkLift
+    , testGroup "JSON-Schema-Test-Suite" $ map (execSchemaTest forkLift) schemaTests
+    , testCase "1-tuple" $ do
+        let
+          schema = empty
+            { schemaType = [Choice1of2 ArrayType]
+            , schemaItems = Just $ Choice2of2 [empty { schemaType = [Choice1of2 NumberType] }]
+            }
+          graph = M.singleton "A" schema
+        (code, _) <- runQ $ generateModule "TestOneTuple" graph
+        result <- typecheck code forkLift
+        case result of
+          Left err -> HU.assertFailure $ show err
+          Right _  -> return ()
+    , testCase "simple map" $ do
+        let
+          schema = [schemaQQ| {
+              "type": "object",
+              "additionalProperties": { "type": "number" }
+            } |]
+          graph = M.singleton "A" schema
+        (code, _) <- runQ $ generateModule "SimpleMap" graph
+        result <- typecheck code forkLift
+        case result of
+          Left err -> HU.assertFailure $ show err
+          Right _  -> return ()
+    ]
+
+typecheckGenerate :: ForkLift -> Schema Text -> Property
+typecheckGenerate forkLift schema = ioProperty $ do
+  let graph = M.singleton "A" schema
+  (code, _) <- runQ $ generateModule "CustomSchema" graph
+  eitherToResult <$> typecheck code forkLift
+
+withCodeTempFile :: Text -> (FilePath -> IO a) -> IO a
+withCodeTempFile code action = case maybeName of
+  Nothing -> fail "couldn't find module name"
+  Just name -> withSystemTempFile (unpack name ++ ".hs") $ \path handle -> do
+    TIO.hPutStrLn handle code
+    hClose handle
+    action path
+  where
+    maybeName = findName (T.lines code)
+    findName (l:ls) = case T.words l of
+      ("module":n:_) -> Just n
+      _ -> findName ls
+    findName _ = Nothing
+
+eitherToResult :: Show err => Either err a -> Result
+eitherToResult (Left err) = failed { reason = show err }
+eitherToResult (Right _)  = succeeded
+
+typecheck :: Text -> ForkLift -> IO (Either Hint.InterpreterError ())
+typecheck code forkLift = withCodeTempFile code $ \path ->
+  carry forkLift $ Hint.loadModules [path]
+
+testExamples :: ForkLift -> [Test]
+testExamples forkLift = examples testCase (assertValid forkLift) (assertInvalid forkLift)
+
+execSchemaTest :: ForkLift -> SchemaTest -> Test
+execSchemaTest forkLift schemaTest = testGroup testName testCases
+  where
+    testName = unpack $ schemaTestDescription schemaTest
+    testCases = map execSchemaTestCase $ schemaTestCases schemaTest
+    schema = schemaTestSchema schemaTest
+    execSchemaTestCase schemaTestCase = testCase name assertion
+      where
+        name = unpack $ schemaTestCaseDescription schemaTestCase
+        shouldBeValid = schemaTestCaseValid schemaTestCase
+        testData = schemaTestCaseData schemaTestCase
+        assertion = assertValidates shouldBeValid forkLift M.empty schema testData
+
+assertValid, assertInvalid :: ForkLift -> Graph Schema Text -> Schema Text -> Value -> HU.Assertion
+assertValid = assertValidates True
+assertInvalid = assertValidates False
+
+assertValidates :: Bool -> ForkLift -> Graph Schema Text -> Schema Text -> Value -> HU.Assertion
+assertValidates shouldBeValid forkLift graph schema value = do
+  let graph' = if M.null graph then M.singleton "a" schema else graph
+  (code, typeMap) <- runQ $ generateModule "TestSchema" graph'
+  valueExpr <- replaceHiddenModules <$> runQ (THS.lift value)
+  let typ = replaceHiddenModules $ typeMap M.! "a"
+  let validatesExpr = unlines
+        [ "case DAT.parseEither parseJSON (" ++ pprint valueExpr ++ ") :: Either String (" ++ pprint typ ++ ") of"
+        , "  Prelude.Left e  -> Just e"
+        , "  Prelude.Right _ -> Nothing"
+        ]
+  result <- withCodeTempFile code $ \path -> carry forkLift $ do
+    Hint.loadModules [path]
+    Hint.setImportsQ $ map (,Nothing) (getUsedModules (valueExpr, typ)) ++
+      [ ("TestSchema", Nothing)
+      , ("Prelude", Nothing)
+      , ("Data.Aeson.Types", Just "DAT")
+      , ("Data.Ratio", Nothing)
+      ]
+    Hint.interpret validatesExpr (Hint.as :: Maybe String)
+  let printInfo = TIO.putStrLn code >> putStrLn validatesExpr
+  case result of
+    Left err -> printInfo >> HU.assertFailure (show err)
+    Right maybeError -> case (shouldBeValid, maybeError) of
+      (True, Nothing) -> return ()
+      (False, Just _) -> return ()
+      (True, Just e)  -> do
+        printInfo
+        HU.assertFailure $ "value should have beean parsed but was rejected with error '" ++ e ++ "'"
+      (False, Nothing) -> do
+        printInfo
+        HU.assertFailure "value should have been rejected"
diff --git a/test/Data/Aeson/Schema/Examples.hs b/test/Data/Aeson/Schema/Examples.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/Schema/Examples.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Data.Aeson.Schema.Examples
+  ( examples
+  ) where
+
+import           Data.Aeson        (Value)
+import           Data.Aeson.LitQQ  (aesonLitQQ)
+import qualified Data.Map          as M
+import           Data.Text         (Text)
+
+import           Data.Aeson.Schema
+
+examples :: (String -> IO () -> a)
+         -> (Graph Schema Text -> Schema Text -> Value -> IO ())
+         -> (Graph Schema Text -> Schema Text -> Value -> IO ())
+         -> [a]
+examples testCase assertValid' assertInvalid' =
+  [ testCase "patternProperties" $ do
+      let schema = [schemaQQ| {
+        "type": "object",
+        "properties": {
+          "positiveNumber": {
+            "type": "number",
+            "minimum": 0,
+            "exclusiveMinimum": true
+          }
+        },
+        "patternProperties": {
+          ".+Number$": { "type": "integer" },
+          ".+String$": { "type": "string" }
+        }
+      } |]
+      assertValid schema [aesonLitQQ| { "positiveNumber": 13 } |]
+      assertInvalid schema [aesonLitQQ| { "positiveNumber": -13 } |]
+      assertInvalid schema [aesonLitQQ| { "positiveNumber": 13.5 } |]
+      assertValid schema [aesonLitQQ| { "fooString": "foo", "barString": "bar" } |]
+      assertInvalid schema [aesonLitQQ| { "fooString": null, "barString": "bar" } |]
+  , testCase "additionalProperties" $ do
+      let additionalNumbers = [schemaQQ| {
+        "type": "object",
+        "properties": { "null": { "type": "null" } },
+        "patternProperties": {
+          ".+String$": { "type": "string" }
+        },
+        "additionalProperties": { "type": "number" }
+      } |]
+      assertValid additionalNumbers [aesonLitQQ| {
+        "null": null,
+        "emptyString": "",
+        "oneMoreThing": 23,
+        "theLastThing": 999
+      } |]
+      assertInvalid additionalNumbers [aesonLitQQ| { "null": null, "notANumber": true } |]
+      let noAdditionalProperties = [schemaQQ| {
+        "type": "object",
+        "properties": { "null": { "type": "null" } },
+        "patternProperties": {
+          ".+String$": { "type": "string" }
+        },
+        "additionalProperties": false
+      } |]
+      assertValid noAdditionalProperties [aesonLitQQ| { "null": null, "emptyString": "" } |]
+      assertInvalid noAdditionalProperties [aesonLitQQ| {
+        "null": null,
+        "emptyString": "",
+        "oneMoreThing": 23,
+        "theLastThing": 999
+      } |]
+  , testCase "disallow" $ do
+      let onlyFloats = [schemaQQ| { "type": "number", "disallow": "integer" } |]
+      assertInvalid onlyFloats [aesonLitQQ| 9 |]
+      assertValid onlyFloats [aesonLitQQ| 9.75 |]
+      let notLengthThree = [schemaQQ| {
+        "type": "array",
+        "disallow": [{
+          "type": "array",
+          "minItems": 3,
+          "maxItems": 3
+        }]
+      } |]
+      assertValid notLengthThree [aesonLitQQ| [] |]
+      assertValid notLengthThree [aesonLitQQ| [1] |]
+      assertValid notLengthThree [aesonLitQQ| [1, 2] |]
+      assertInvalid notLengthThree [aesonLitQQ| [1, 2, 3] |]
+      assertValid notLengthThree [aesonLitQQ| [1, 2, 3, 4] |]
+      let everythingExceptNumbers = [schemaQQ| { "disallow": "number" } |]
+      assertInvalid everythingExceptNumbers [aesonLitQQ| 3 |]
+      assertInvalid everythingExceptNumbers [aesonLitQQ| 3.5 |]
+      assertValid everythingExceptNumbers [aesonLitQQ| true |]
+      assertValid everythingExceptNumbers [aesonLitQQ| "nobody expects the ..." |]
+      assertValid everythingExceptNumbers [aesonLitQQ| { "eins": 1, "zwei": 2 } |]
+      assertValid everythingExceptNumbers [aesonLitQQ| ["eins", "zwei"] |]
+      assertValid everythingExceptNumbers [aesonLitQQ| null |]
+  , testCase "type: subschema" $ do
+      let schema = [schemaQQ| {
+        "type": [
+          {
+            "type": "object",
+            "properties": { "insert": { "type": "string", "minLength": 1 } },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": { "delete": { "type": "number", "minimum": 1 } },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": { "retain": { "type": "number", "minimum": 1 } },
+            "additionalProperties": false
+          }
+        ]
+      } |]
+      assertValid schema [aesonLitQQ| { "insert": "lorem" } |]
+      assertInvalid schema [aesonLitQQ| { "insert": "lorem", "delete": 5 } |]
+      assertValid schema [aesonLitQQ| { "delete": 5 } |]
+      assertInvalid schema [aesonLitQQ| { "delete": 5, "retain": 76 } |]
+      assertValid schema [aesonLitQQ| { "retain": 76 } |]
+  , testCase "dependencies" $ do
+      let aRequiresB = [schemaQQ| {
+        "type": "object",
+        "dependencies": { "a": "b" }
+      } |]
+      assertValid aRequiresB [aesonLitQQ| {} |]
+      assertValid aRequiresB [aesonLitQQ| { "b": false } |]
+      assertValid aRequiresB [aesonLitQQ| { "a": true, "b": false } |]
+      assertInvalid aRequiresB [aesonLitQQ| { "a": 3 } |]
+      let aRequiresBToBeANumber = [schemaQQ| {
+        "type": "object",
+        "dependencies": { "a": { "properties": { "b": { "type": "number" } } } }
+      } |]
+      assertValid aRequiresBToBeANumber [aesonLitQQ| {} |]
+      assertValid aRequiresBToBeANumber [aesonLitQQ| { "b": "lorem" } |]
+      assertValid aRequiresBToBeANumber [aesonLitQQ| { "a": "yes, we can" } |]
+      assertInvalid aRequiresBToBeANumber [aesonLitQQ| { "a": "yes, we can", "b": "lorem" } |]
+      assertValid aRequiresBToBeANumber [aesonLitQQ| { "a": "hi there", "b": 42 } |]
+      let aDisallowsB = [schemaQQ| {
+        "type": "object",
+        "dependencies": {
+          "a": {
+            "disallow": [{
+              "properties": {
+                "b": { "type": "any", "required": true }
+              }
+            }]
+          }
+        }
+      } |]
+      assertValid aDisallowsB [aesonLitQQ| { "a": "lorem" } |]
+      assertValid aDisallowsB [aesonLitQQ| { "b": 42 } |]
+      assertInvalid aDisallowsB [aesonLitQQ| { "a": "lorem", "b": 42 } |]
+  , testCase "extends" $ do
+      let schema = [schemaQQ| {
+        "type": "object",
+        "properties": {
+          "a": { "type": "number" }
+        },
+        "extends": [
+          {
+            "properties": {
+              "a": { "required": true }
+            }
+          },
+          {
+            "patternProperties": {
+              "^[a-z]$": { "minimum": -3 }
+            }
+          }
+        ]
+      } |]
+      assertValid schema [aesonLitQQ| { "a": 2 } |]
+      assertInvalid schema [aesonLitQQ| {} |]
+      assertInvalid schema [aesonLitQQ| { "a": -4 } |]
+      assertInvalid schema [aesonLitQQ| { "a": "foo" } |]
+      assertInvalid schema [aesonLitQQ| { "a": -1, "b": -10 } |]
+      assertValid schema [aesonLitQQ| { "a": -1, "ba": -10 } |]
+  , testCase "$ref" $ do
+      let
+        a = [schemaQQ| { "$ref": "b", "minimum": 3 } |]
+        b = [schemaQQ| { "type": "number", "maximum": 2 } |]
+        graph = M.fromList [("a", a), ("b", b)]
+      assertValid' graph a [aesonLitQQ| 1 |]
+      assertInvalid' graph a [aesonLitQQ| 4 |]
+  ]
+  where
+    assertValid = assertValid' M.empty
+    assertInvalid = assertInvalid' M.empty
diff --git a/test/Data/Aeson/Schema/Types/Tests.hs b/test/Data/Aeson/Schema/Types/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/Schema/Types/Tests.hs
@@ -0,0 +1,49 @@
+module Data.Aeson.Schema.Types.Tests
+  ( tests
+  ) where
+
+import           Test.Framework
+import           Test.Framework.Providers.HUnit
+import qualified Test.HUnit                     as HU
+
+import           Data.Aeson
+import qualified Data.ByteString.Lazy           as L
+import           Data.Foldable                  (toList)
+import qualified Data.HashMap.Strict            as H
+import           Data.Text                      (Text)
+
+import           Data.Aeson.Schema
+import           Data.Aeson.Schema.Choice
+
+data TestFunctor a = TestFunctor Int a
+
+instance Functor TestFunctor where
+  fmap f (TestFunctor i a) = TestFunctor i (f a)
+
+tests :: [Test]
+tests =
+  [ testCase "parse schema.json" $ do
+      schemaBS <- L.readFile "examples/schema.json"
+      case decode schemaBS :: Maybe Value of
+        Nothing -> HU.assertFailure "JSON syntax error"
+        Just val -> case fromJSON val :: Result (Schema Text) of
+          Error e -> HU.assertFailure e
+          Success schema -> do
+            Just "http://json-schema.org/schema#" HU.@=? schemaId schema
+            0 HU.@=? schemaMinItems schema
+  , testCase "Foldable instance" $ do
+      let schemaWithRef ref = empty { schemaDRef = Just ref }
+      let schema = empty
+            { schemaType = [Choice2of2 $ schemaWithRef "a"]
+            , schemaProperties = H.fromList [("aProperty", schemaWithRef "b")]
+            , schemaPatternProperties = let Right p = mkPattern "lorem.+" in [(p, schemaWithRef "c")]
+            , schemaAdditionalProperties = Choice2of2 $ schemaWithRef "d"
+            , schemaItems = Just $ Choice2of2 [schemaWithRef "e", schemaWithRef "f"]
+            , schemaAdditionalItems = Choice2of2 $ schemaWithRef "g"
+            , schemaDependencies = H.fromList [("aProperty", Choice2of2 $ schemaWithRef "h")]
+            , schemaDisallow = [Choice2of2 $ schemaWithRef "i"]
+            , schemaExtends = [schemaWithRef "j"]
+            , schemaDRef = Just "k"
+            }
+      map (:[]) ['a'..'k'] HU.@=? toList schema
+  ]
diff --git a/test/Data/Aeson/Schema/Validator/Tests.hs b/test/Data/Aeson/Schema/Validator/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Aeson/Schema/Validator/Tests.hs
@@ -0,0 +1,48 @@
+module Data.Aeson.Schema.Validator.Tests
+  ( tests
+  ) where
+
+import           Test.Framework
+import           Test.Framework.Providers.HUnit
+import qualified Test.HUnit                     as HU
+
+import           Data.Aeson                     (Value)
+import qualified Data.Map                       as M
+import           Data.Text                      (Text, unpack)
+
+import           Data.Aeson.Schema.Types
+import           Data.Aeson.Schema.Validator
+
+import           Data.Aeson.Schema.Examples     (examples)
+import           TestSuite.Types                (SchemaTest (..),
+                                                 SchemaTestCase (..))
+
+assertValid, assertInvalid :: Graph Schema Text
+                           -> Schema Text
+                           -> Value
+                           -> HU.Assertion
+assertValid graph schema value = case validate graph schema value of
+  [] -> return ()
+  es -> HU.assertFailure $ unlines es
+assertInvalid graph schema value = case validate graph schema value of
+  [] -> HU.assertFailure "expected a validation error"
+  _  -> return ()
+
+tests :: [SchemaTest] -> [Test]
+tests schemaTests = examples testCase assertValid assertInvalid ++ map buildSchemaTest schemaTests
+  where
+    buildSchemaTest :: SchemaTest -> Test
+    buildSchemaTest schemaTest = testGroup testName cases
+      where
+        testName = unpack $ schemaTestDescription schemaTest
+        cases = map (buildSchemaTestCase $ schemaTestSchema schemaTest) $ schemaTestCases schemaTest
+    buildSchemaTestCase :: Schema Text -> SchemaTestCase -> Test
+    buildSchemaTestCase schema schemaTestCase = testCase testName assertion
+      where
+        testName = unpack $ schemaTestCaseDescription schemaTestCase
+        testData = schemaTestCaseData schemaTestCase
+        graph = M.empty
+        assertion = if schemaTestCaseValid schemaTestCase
+          then assertValid graph schema testData
+          else assertInvalid graph schema testData
+
diff --git a/test/TestSuite/Types.hs b/test/TestSuite/Types.hs
new file mode 100644
--- /dev/null
+++ b/test/TestSuite/Types.hs
@@ -0,0 +1,64 @@
+module TestSuite.Types
+  ( SchemaTest (..)
+  , SchemaTestCase (..)
+  , readSchemaTests
+  ) where
+
+import           Control.Applicative              ((*>), (<$>), (<*), (<*>))
+import           Control.Monad                    (forM)
+import           Data.Aeson
+import           Data.Aeson.Schema
+import           Data.Aeson.Types                 (parseEither)
+import           Data.Attoparsec.ByteString.Char8 (skipSpace)
+import           Data.Attoparsec.Lazy             (Result (..), parse)
+import qualified Data.ByteString.Lazy             as LBS
+import           Data.List                        (isSuffixOf)
+import           Data.Maybe                       (fromMaybe)
+import           Data.Monoid                      ((<>))
+import           Data.Text                        (Text, pack)
+import           Paths_aeson_schema               (getDataFileName)
+import           System.Directory                 (getDirectoryContents)
+import           System.FilePath                  ((</>))
+
+data SchemaTest = SchemaTest
+  { schemaTestDescription :: Text
+  , schemaTestSchema      :: Schema Text
+  , schemaTestCases       :: [SchemaTestCase]
+  } deriving (Eq, Show)
+
+instance FromJSON SchemaTest where
+  parseJSON (Object o) = SchemaTest <$> (fromMaybe "" <$> o .:? "description")
+                                    <*> (o .: "schema")
+                                    <*> (o .: "tests")
+  parseJSON _ = fail "expected an object"
+
+data SchemaTestCase = SchemaTestCase
+  { schemaTestCaseDescription :: Text
+  , schemaTestCaseData        :: Value
+  , schemaTestCaseValid       :: Bool
+  } deriving (Eq, Show)
+
+instance FromJSON SchemaTestCase where
+  parseJSON (Object o) = SchemaTestCase <$> (o .: "description")
+                                        <*> (o .: "data")
+                                        <*> (o .: "valid")
+  parseJSON _ = fail "expected an object"
+
+-- | Read tests collected by Julian Berman (https://github.com/Julian/JSON-Schema-Test-Suite)
+readSchemaTests :: FilePath -> IO [SchemaTest]
+readSchemaTests dir' = do
+  dir <- getDataFileName dir'
+  contents <- getDirectoryContents dir
+  let jsonFiles = filter (".json" `isSuffixOf`) contents
+  fmap concat $ forM jsonFiles $ \file -> do
+    let fullPath = dir </> file
+    jsonBS <- LBS.readFile fullPath
+    case parse (skipSpace *> json <* skipSpace) jsonBS of
+      Done _ value -> case parseEither parseJSON value of
+        Left err -> fail $ "couldn't parse file '" ++ fullPath ++ "': " ++ err
+        Right v  -> return $ map (prependFileName file) v
+      _ -> fail $ "not a valid json file: " ++ fullPath
+  where
+    prependFileName fileName schemaTest = schemaTest
+      { schemaTestDescription = pack fileName <> ": " <> schemaTestDescription schemaTest
+      }
diff --git a/test/test-suite/tests/draft3/additionalItems.json b/test/test-suite/tests/draft3/additionalItems.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/additionalItems.json
@@ -0,0 +1,67 @@
+[
+    {
+        "description": "additionalItems as schema",
+        "schema": {
+            "items": [],
+            "additionalItems": {"type": "integer"}
+        },
+        "tests": [
+            {
+                "description": "additional items match schema",
+                "data": [ 1, 2, 3, 4 ],
+                "valid": true
+            },
+            {
+                "description": "additional items do not match schema",
+                "data": [ 1, 2, 3, "foo" ],
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "items is schema, no additionalItems",
+        "schema": {
+            "items": {},
+            "additionalItems": false
+        },
+        "tests": [
+            {
+                "description": "all items match schema",
+                "data": [ 1, 2, 3, 4, 5 ],
+                "valid": true
+            }
+        ]
+    },
+    {
+        "description": "array of items with no additionalItems",
+        "schema": {
+            "items": [{}, {}, {}],
+            "additionalItems": false
+        },
+        "tests": [
+            {
+                "description": "no additional items present",
+                "data": [ 1, 2, 3 ],
+                "valid": true
+            },
+            {
+                "description": "additional items are not permitted",
+                "data": [ 1, 2, 3, 4 ],
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "additionalItems as false without items",
+        "schema": {
+            "additionalItems": false
+        },
+        "tests": [
+            {
+                "description": "items should allow anything",
+                "data": [ 1, 2, 3, 4, 5 ],
+                "valid": true
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/additionalProperties.json b/test/test-suite/tests/draft3/additionalProperties.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/additionalProperties.json
@@ -0,0 +1,58 @@
+[
+    {
+        "description":
+            "additionalProperties being false does not allow other properties",
+        "schema": {
+            "properties": {"foo": {}, "bar": {}},
+            "additionalProperties": false
+        },
+        "tests": [
+            {
+                "description": "no additional properties is valid",
+                "data": {"foo": 1},
+                "valid": true
+            },
+            {
+                "description": "an additional property is invalid",
+                "data": {"foo" : 1, "bar" : 2, "quux" : "boom"},
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description":
+            "additionalProperties allows a schema which should validate",
+        "schema": {
+            "properties": {"foo": {}, "bar": {}},
+            "additionalProperties": {"type": "boolean"}
+        },
+        "tests": [
+            {
+                "description": "no additional properties is valid",
+                "data": {"foo": 1},
+                "valid": true
+            },
+            {
+                "description": "an additional valid property is valid",
+                "data": {"foo" : 1, "bar" : 2, "quux" : true},
+                "valid": true
+            },
+            {
+                "description": "an additional invalid property is invalid",
+                "data": {"foo" : 1, "bar" : 2, "quux" : 12},
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "additionalProperties are allowed by default",
+        "schema": {"properties": {"foo": {}, "bar": {}}},
+        "tests": [
+            {
+                "description": "additional properties are allowed",
+                "data": {"foo": 1, "bar": 2, "quux": true},
+                "valid": true
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/divisibleBy.json b/test/test-suite/tests/draft3/divisibleBy.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/divisibleBy.json
@@ -0,0 +1,23 @@
+[
+    {
+        "description": "divisibleBy validation",
+        "schema": {"divisibleBy": 1.5},
+        "tests": [
+            {
+                "description": "zero is divisible by anything (except 0)",
+                "data": 0,
+                "valid": true
+            },
+            {
+                "description": "4.5 is divisible by 1.5",
+                "data": 4.5,
+                "valid": true
+            },
+            {
+                "description": "35 is not divisible by 1.5",
+                "data": 35,
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/enum.json b/test/test-suite/tests/draft3/enum.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/enum.json
@@ -0,0 +1,39 @@
+[
+    {
+        "description": "simple enum validation",
+        "schema": {"enum": [1, 2, 3]},
+        "tests": [
+            {
+                "description": "one of the enum is valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "something else is invalid",
+                "data": 4,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "heterogeneous enum validation",
+        "schema": {"enum": [1, "foo", [], true, {"foo": 12}]},
+        "tests": [
+            {
+                "description": "one of the enum is valid",
+                "data": [],
+                "valid": true
+            },
+            {
+                "description": "something else is invalid",
+                "data": null,
+                "valid": false
+            },
+            {
+                "description": "objects are deep compared",
+                "data": {"foo": false},
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/items.json b/test/test-suite/tests/draft3/items.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/items.json
@@ -0,0 +1,41 @@
+[
+    {
+        "description": "a schema given for items",
+        "schema": {
+            "items": {"type": "integer"}
+        },
+        "tests": [
+            {
+                "description": "valid items",
+                "data": [ 1, 2, 3 ],
+                "valid": true
+            },
+            {
+                "description": "wrong type of items",
+                "data": [1, "x"],
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "an array of schemas for items",
+        "schema": {
+            "items": [
+                {"type": "integer"},
+                {"type": "string"}
+            ]
+        },
+        "tests": [
+            {
+                "description": "correct types",
+                "data": [ 1, "foo" ],
+                "valid": true
+            },
+            {
+                "description": "wrong types",
+                "data": [ "foo", 1 ],
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/maxItems.json b/test/test-suite/tests/draft3/maxItems.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/maxItems.json
@@ -0,0 +1,23 @@
+[
+    {
+        "description": "maxItems validation",
+        "schema": {"maxItems": 2},
+        "tests": [
+            {
+                "description": "shorter is valid",
+                "data": [1],
+                "valid": true
+            },
+            {
+                "description": "exact length is valid",
+                "data": [1, 2],
+                "valid": true
+            },
+            {
+                "description": "too long is invalid",
+                "data": [1, 2, 3],
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/maxLength.json b/test/test-suite/tests/draft3/maxLength.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/maxLength.json
@@ -0,0 +1,23 @@
+[
+    {
+        "description": "maxLength validation",
+        "schema": {"maxLength": 2},
+        "tests": [
+            {
+                "description": "shorter is valid",
+                "data": "f",
+                "valid": true
+            },
+            {
+                "description": "exact length is valid",
+                "data": "fo",
+                "valid": true
+            },
+            {
+                "description": "too long is invalid",
+                "data": "foo",
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/maximum.json b/test/test-suite/tests/draft3/maximum.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/maximum.json
@@ -0,0 +1,37 @@
+[
+    {
+        "description": "maximum validation",
+        "schema": {"maximum": 3.0},
+        "tests": [
+            {
+                "description": "below the maximum is valid",
+                "data": 2.6,
+                "valid": true
+            },
+            {
+                "description": "above the maximum is invalid",
+                "data": 3.5,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "exclusiveMaximum validation",
+        "schema": {
+            "maximum": 3.0,
+            "exclusiveMaximum": true
+        },
+        "tests": [
+            {
+                "description": "below the maximum is still valid",
+                "data": 2.2,
+                "valid": true
+            },
+            {
+                "description": "boundary point is invalid",
+                "data": 3.0,
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/minItems.json b/test/test-suite/tests/draft3/minItems.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/minItems.json
@@ -0,0 +1,23 @@
+[
+    {
+        "description": "minItems validation",
+        "schema": {"minItems": 1},
+        "tests": [
+            {
+                "description": "longer is valid",
+                "data": [1, 2],
+                "valid": true
+            },
+            {
+                "description": "exact length is valid",
+                "data": [1],
+                "valid": true
+            },
+            {
+                "description": "too short is invalid",
+                "data": [],
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/minLength.json b/test/test-suite/tests/draft3/minLength.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/minLength.json
@@ -0,0 +1,23 @@
+[
+    {
+        "description": "minLength validation",
+        "schema": {"minLength": 2},
+        "tests": [
+            {
+                "description": "longer is valid",
+                "data": "foo",
+                "valid": true
+            },
+            {
+                "description": "exact length is valid",
+                "data": "fo",
+                "valid": true
+            },
+            {
+                "description": "too short is invalid",
+                "data": "f",
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/minimum.json b/test/test-suite/tests/draft3/minimum.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/minimum.json
@@ -0,0 +1,37 @@
+[
+    {
+        "description": "minimum validation",
+        "schema": {"minimum": 1.1},
+        "tests": [
+            {
+                "description": "above the minimum is valid",
+                "data": 2.6,
+                "valid": true
+            },
+            {
+                "description": "below the minimum is invalid",
+                "data": 0.6,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "exclusiveMinimum validation",
+        "schema": {
+            "minimum": 1.1,
+            "exclusiveMinimum": true
+        },
+        "tests": [
+            {
+                "description": "above the minimum is still valid",
+                "data": 1.2,
+                "valid": true
+            },
+            {
+                "description": "boundary point is invalid",
+                "data": 1.1,
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/optional/format.json b/test/test-suite/tests/draft3/optional/format.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/optional/format.json
@@ -0,0 +1,18 @@
+[
+    {
+        "description": "validation of regular expressions",
+        "schema": {"format": "regex"},
+        "tests": [
+            {
+                "description": "a valid regular expression",
+                "data": "([abc])+\\s+$",
+                "valid": true
+            },
+            {
+                "description": "a regular expression with unclosed parens is invalid",
+                "data": "^(abc]",
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/pattern.json b/test/test-suite/tests/draft3/pattern.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/pattern.json
@@ -0,0 +1,18 @@
+[
+    {
+        "description": "pattern validation",
+        "schema": {"pattern": "^a*$"},
+        "tests": [
+            {
+                "description": "a matching pattern is valid",
+                "data": "aaa",
+                "valid": true
+            },
+            {
+                "description": "a non-matching pattern is invalid",
+                "data": "abc",
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/patternProperties.json b/test/test-suite/tests/draft3/patternProperties.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/patternProperties.json
@@ -0,0 +1,74 @@
+[
+    {
+        "description":
+            "patternProperties validates properties matching a regex",
+        "schema": {
+            "patternProperties": {
+                "f.*o": {"type": "integer"}
+            }
+        },
+        "tests": [
+            {
+                "description": "a single valid match is valid",
+                "data": {"foo": 1},
+                "valid": true
+            },
+            {
+                "description": "multiple valid matches is valid",
+                "data": {"foo": 1, "foooooo" : 2},
+                "valid": true
+            },
+            {
+                "description": "a single invalid match is invalid",
+                "data": {"foo": "bar", "fooooo": 2},
+                "valid": false
+            },
+            {
+                "description": "multiple invalid matches is invalid",
+                "data": {"foo": "bar", "foooooo" : "baz"},
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "multiple simulatneous patternProperties are validated",
+        "schema": {
+            "patternProperties": {
+                "a*": {"type": "integer"},
+                "aaa*": {"maximum": 20}
+            }
+        },
+        "tests": [
+            {
+                "description": "a single valid match is valid",
+                "data": {"a": 21},
+                "valid": true
+            },
+            {
+                "description": "a simultaneous match is valid",
+                "data": {"aaaa": 18},
+                "valid": true
+            },
+            {
+                "description": "multiple matches is valid",
+                "data": {"a": 21, "aaaa": 18},
+                "valid": true
+            },
+            {
+                "description": "an invalid due to one is invalid",
+                "data": {"a": "bar"},
+                "valid": false
+            },
+            {
+                "description": "an invalid due to the other is invalid",
+                "data": {"aaaa": 31},
+                "valid": false
+            },
+            {
+                "description": "an invalid due to both is invalid",
+                "data": {"aaa": "foo", "aaaa": 31},
+                "valid": false
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/properties.json b/test/test-suite/tests/draft3/properties.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/properties.json
@@ -0,0 +1,33 @@
+[
+    {
+        "description": "object properties validation",
+        "schema": {
+            "properties": {
+                "foo": {"type": "integer"},
+                "bar": {"type": "string"}
+            }
+        },
+        "tests": [
+            {
+                "description": "both properties present and valid is valid",
+                "data": {"foo": 1, "bar": "baz"},
+                "valid": true
+            },
+            {
+                "description": "one property invalid is invalid",
+                "data": {"foo": 1, "bar": {}},
+                "valid": false
+            },
+            {
+                "description": "both properties invalid is invalid",
+                "data": {"foo": [], "bar": {}},
+                "valid": false
+            },
+            {
+                "description": "doesn't invalidate other properties",
+                "data": {"quux": []},
+                "valid": true
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/required.json b/test/test-suite/tests/draft3/required.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/required.json
@@ -0,0 +1,53 @@
+[
+    {
+        "description": "required validation",
+        "schema": {
+            "properties": {
+                "foo": {"required" : true},
+                "bar": {}
+            }
+        },
+        "tests": [
+            {
+                "description": "present required property is valid",
+                "data": {"foo": 1},
+                "valid": true
+            },
+            {
+                "description": "non-present required property is invalid",
+                "data": {"bar": 1},
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "required default validation",
+        "schema": {
+            "properties": {
+                "foo": {}
+            }
+        },
+        "tests": [
+            {
+                "description": "not required by default",
+                "data": {},
+                "valid": true
+            }
+        ]
+    },
+    {
+        "description": "required explicitly false validation",
+        "schema": {
+            "properties": {
+                "foo": {"required": false}
+            }
+        },
+        "tests": [
+            {
+                "description": "not required if required is false",
+                "data": {},
+                "valid": true
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/type.json b/test/test-suite/tests/draft3/type.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/type.json
@@ -0,0 +1,474 @@
+[
+    {
+        "description": "integer type matches integers",
+        "schema": {"type": "integer"},
+        "tests": [
+            {
+                "description": "an integer is an integer",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "a float is not an integer",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is not an integer",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is not an integer",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not an integer",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not an integer",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is not an integer",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "number type matches numbers",
+        "schema": {"type": "number"},
+        "tests": [
+            {
+                "description": "an integer is a number",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "a float is a number",
+                "data": 1.1,
+                "valid": true
+            },
+            {
+                "description": "a string is not a number",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is not a number",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not a number",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not a number",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is not a number",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "string type matches strings",
+        "schema": {"type": "string"},
+        "tests": [
+            {
+                "description": "1 is not a string",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a float is not a string",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is a string",
+                "data": "foo",
+                "valid": true
+            },
+            {
+                "description": "an object is not a string",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not a string",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not a string",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is not a string",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "object type matches objects",
+        "schema": {"type": "object"},
+        "tests": [
+            {
+                "description": "an integer is not an object",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a float is not an object",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is not an object",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is an object",
+                "data": {},
+                "valid": true
+            },
+            {
+                "description": "an array is not an object",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not an object",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is not an object",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "array type matches arrays",
+        "schema": {"type": "array"},
+        "tests": [
+            {
+                "description": "an integer is not an array",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a float is not an array",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is not an array",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is not an array",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not an array",
+                "data": [],
+                "valid": true
+            },
+            {
+                "description": "a boolean is not an array",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is not an array",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "boolean type matches booleans",
+        "schema": {"type": "boolean"},
+        "tests": [
+            {
+                "description": "an integer is not a boolean",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a float is not a boolean",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is not a boolean",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is not a boolean",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not a boolean",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not a boolean",
+                "data": true,
+                "valid": true
+            },
+            {
+                "description": "null is not a boolean",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "null type matches only the null object",
+        "schema": {"type": "null"},
+        "tests": [
+            {
+                "description": "an integer is not null",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a float is not null",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "a string is not null",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "an object is not null",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is not null",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is not null",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is null",
+                "data": null,
+                "valid": true
+            }
+        ]
+    },
+    {
+        "description": "any type matches any type",
+        "schema": {"type": "any"},
+        "tests": [
+            {
+                "description": "any type includes integers",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "any type includes float",
+                "data": 1.1,
+                "valid": true
+            },
+            {
+                "description": "any type includes string",
+                "data": "foo",
+                "valid": true
+            },
+            {
+                "description": "any type includes object",
+                "data": {},
+                "valid": true
+            },
+            {
+                "description": "any type includes array",
+                "data": [],
+                "valid": true
+            },
+            {
+                "description": "any type includes boolean",
+                "data": true,
+                "valid": true
+            },
+            {
+                "description": "any type includes null",
+                "data": null,
+                "valid": true
+            }
+        ]
+    },
+    {
+        "description": "multiple types can be specified in an array",
+        "schema": {"type": ["integer", "string"]},
+        "tests": [
+            {
+                "description": "an integer is valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "a string is valid",
+                "data": "foo",
+                "valid": true
+            },
+            {
+                "description": "a float is invalid",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "an object is invalid",
+                "data": {},
+                "valid": false
+            },
+            {
+                "description": "an array is invalid",
+                "data": [],
+                "valid": false
+            },
+            {
+                "description": "a boolean is invalid",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is invalid",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "types can include schemas",
+        "schema": {
+            "type": [
+                "array",
+                {"type": "object"}
+            ]
+        },
+        "tests": [
+            {
+                "description": "an integer is invalid",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a string is invalid",
+                "data": "foo",
+                "valid": false
+            },
+            {
+                "description": "a float is invalid",
+                "data": 1.1,
+                "valid": false
+            },
+            {
+                "description": "an object is valid",
+                "data": {},
+                "valid": true
+            },
+            {
+                "description": "an array is valid",
+                "data": [],
+                "valid": true
+            },
+            {
+                "description": "a boolean is invalid",
+                "data": true,
+                "valid": false
+            },
+            {
+                "description": "null is invalid",
+                "data": null,
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": 
+            "when types includes a schema it should fully validate the schema",
+        "schema": {
+            "type": [
+                "integer",
+                {
+                    "properties": {
+                        "foo": {"type": "null"}
+                    }
+                }
+            ]
+        },
+        "tests": [
+            {
+                "description": "an integer is valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "an object is valid only if it is fully valid",
+                "data": {"foo": null},
+                "valid": true
+            },
+            {
+                "description": "an object is invalid otherwise",
+                "data": {"foo": "bar"},
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "types from separate schemas are merged",
+        "schema": {
+            "type": [
+                {"type": ["string"]},
+                {"type": ["array", "null"]}
+            ]
+        },
+        "tests": [
+            {
+                "description": "an integer is invalid",
+                "data": 1,
+                "valid": false
+            },
+            {
+                "description": "a string is valid",
+                "data": "foo",
+                "valid": true
+            },
+            {
+                "description": "an array is valid",
+                "data": [1, 2, 3],
+                "valid": true
+            }
+        ]
+    }
+]
diff --git a/test/test-suite/tests/draft3/uniqueItems.json b/test/test-suite/tests/draft3/uniqueItems.json
new file mode 100644
--- /dev/null
+++ b/test/test-suite/tests/draft3/uniqueItems.json
@@ -0,0 +1,75 @@
+[
+    {
+        "description": "uniqueItems validation",
+        "schema": {"uniqueItems": true},
+        "tests": [
+            {
+                "description": "unique array of integers is valid",
+                "data": [1, 2],
+                "valid": true
+            },
+            {
+                "description": "non-unique array of integers is invalid",
+                "data": [1, 1],
+                "valid": false
+            },
+            {
+                "description": "unique array of objects is valid",
+                "data": [{"foo": "bar"}, {"foo": "baz"}],
+                "valid": true
+            },
+            {
+                "description": "non-unique array of objects is invalid",
+                "data": [{"foo": "bar"}, {"foo": "bar"}],
+                "valid": false
+            },
+            {
+                "description": "unique array of nested objects is valid",
+                "data": [
+                    {"foo": {"bar" : {"baz" : true}}},
+                    {"foo": {"bar" : {"baz" : false}}}
+                ],
+                "valid": true
+            },
+            {
+                "description": "non-unique array of nested objects is invalid",
+                "data": [
+                    {"foo": {"bar" : {"baz" : true}}},
+                    {"foo": {"bar" : {"baz" : true}}}
+                ],
+                "valid": false
+            },
+            {
+                "description": "unique array of arrays is valid",
+                "data": [["foo"], ["bar"]],
+                "valid": true
+            },
+            {
+                "description": "non-unique array of arrays is invalid",
+                "data": [["foo"], ["foo"]],
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "heterogeneous enum validation",
+        "schema": {"enum": [1, "foo", [], true, {"foo": 12}]},
+        "tests": [
+            {
+                "description": "one of the enum is valid",
+                "data": [],
+                "valid": true
+            },
+            {
+                "description": "something else is invalid",
+                "data": null,
+                "valid": false
+            },
+            {
+                "description": "objects are deep compared",
+                "data": {"foo": false},
+                "valid": false
+            }
+        ]
+    }
+]
