diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.markdown
@@ -0,0 +1,7 @@
+# Change log
+
+Derulo uses [Semantic Versioning][].
+The change log is available through the [releases on GitHub][].
+
+[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
+[releases on GitHub]: https://github.com/tfausak/derulo/releases
diff --git a/LICENSE.markdown b/LICENSE.markdown
new file mode 100644
--- /dev/null
+++ b/LICENSE.markdown
@@ -0,0 +1,21 @@
+# [The MIT License](https://opensource.org/licenses/MIT)
+
+Copyright 2017 Taylor Fausak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,3 @@
+# Derulo
+
+Derulo parses and renders JSON simply.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+import qualified Distribution.Simple as Cabal
+
+main :: IO ()
+main = Cabal.defaultMain
diff --git a/derulo.cabal b/derulo.cabal
new file mode 100644
--- /dev/null
+++ b/derulo.cabal
@@ -0,0 +1,49 @@
+-- This file has been generated from package.yaml by hpack version 0.17.1.
+--
+-- see: https://github.com/sol/hpack
+
+name:           derulo
+version:        0.0.0
+synopsis:       Parse and render JSON simply.
+description:    Derulo parses and renders JSON simply.
+category:       JSON
+maintainer:     Taylor Fausak
+license:        MIT
+license-file:   LICENSE.markdown
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    CHANGELOG.markdown
+    README.markdown
+
+library
+  hs-source-dirs:
+      library
+  ghc-options: -Wall
+  build-depends:
+      base >=4.9.1 && <4.10
+  exposed-modules:
+      Derulo
+  default-language: Haskell2010
+
+executable derulo
+  main-is: Main.hs
+  hs-source-dirs:
+      executables
+  ghc-options: -Wall -rtsopts -threaded
+  build-depends:
+      base >=4.9.1 && <4.10
+    , derulo
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+      tests
+  ghc-options: -Wall -rtsopts -threaded
+  build-depends:
+      base >=4.9.1 && <4.10
+    , doctest >=0.11.4 && <0.12
+  default-language: Haskell2010
diff --git a/executables/Main.hs b/executables/Main.hs
new file mode 100644
--- /dev/null
+++ b/executables/Main.hs
@@ -0,0 +1,5 @@
+import qualified Data.Maybe as Maybe
+import qualified Derulo
+
+main :: IO ()
+main = interact (Derulo.showJSON . Maybe.fromJust . Derulo.readJSON)
diff --git a/library/Derulo.hs b/library/Derulo.hs
new file mode 100644
--- /dev/null
+++ b/library/Derulo.hs
@@ -0,0 +1,480 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+-- | Derulo parses and renders JSON simply. It aims to provide an RFC 7159
+-- compliant parser and renderer without incurring any dependencies. It is
+-- intended to be used either for learning or in situations where dependencies
+-- are unwanted. In normal usage, prefer a faster, more robust library like
+-- [Aeson](https://hackage.haskell.org/package/aeson).
+--
+-- Derulo does not export any identifiers that conflict with the prelude and
+-- can be imported unqualified.
+--
+-- >>> import Derulo
+--
+-- Use 'readJSON' to parse a 'String' into a 'JSON' value.
+--
+-- >>> readJSON " null "
+-- Just Null
+--
+-- Use 'showJSON' to render a 'JSON' value as a 'String'.
+--
+-- >>> showJSON Null
+-- "null"
+module Derulo where
+
+import qualified Control.Monad as Monad
+import qualified Data.Data as Data
+import qualified Data.Functor as Functor
+import qualified Data.List as List
+import qualified Data.Maybe as Maybe
+import qualified GHC.Generics as Generics
+import qualified Text.ParserCombinators.ReadP as ReadP
+
+-- * Types
+-- | A JSON value as described by RFC 7159.
+data JSON
+  = Null
+  | Boolean Bool
+  | Number Integer
+           Integer
+  | String String
+  | Array [JSON]
+  | Object [(String, JSON)]
+  deriving (Data.Data, Eq, Generics.Generic, Ord, Read, Show, Data.Typeable)
+
+-- * Parsing
+-- | Parses a string as JSON.
+--
+-- >>> readJSON "null"
+-- Just Null
+--
+-- >>> readJSON "true"
+-- Just (Boolean True)
+-- >>> readJSON "false"
+-- Just (Boolean False)
+--
+-- >>> readJSON "0e0"
+-- Just (Number 0 0)
+-- >>> readJSON "12e34"
+-- Just (Number 12 34)
+-- >>> readJSON "-12e-34"
+-- Just (Number (-12) (-34))
+--
+-- >>> readJSON "\"\""
+-- Just (String "")
+-- >>> readJSON "\"js\""
+-- Just (String "js")
+-- >>> readJSON "\"\\\"\\\\\\b\\f\\n\\r\\t\""
+-- Just (String "\"\\\b\f\n\r\t")
+-- >>> readJSON "\"\\u001f\""
+-- Just (String "\US")
+--
+-- >>> readJSON "[]"
+-- Just (Array [])
+-- >>> readJSON "[null]"
+-- Just (Array [Null])
+-- >>> readJSON "[true,false]"
+-- Just (Array [Boolean True,Boolean False])
+--
+-- >>> readJSON "{}"
+-- Just (Object [])
+-- >>> readJSON "{\"\":null}"
+-- Just (Object [("",Null)])
+-- >>> readJSON "{\"t\":true,\"f\":false}"
+-- Just (Object [("t",Boolean True),("f",Boolean False)])
+readJSON :: String -> Maybe JSON
+readJSON string = runParser pJSON string
+
+pJSON :: ReadP.ReadP JSON
+pJSON = do
+  pWhitespaces
+  value <- pValue
+  ReadP.eof
+  pure value
+
+pValue :: ReadP.ReadP JSON
+pValue = ReadP.choice [pNull, pBoolean, pNumber, pString, pArray, pObject]
+
+pNull :: ReadP.ReadP JSON
+pNull = do
+  pSymbol "null"
+  pure Null
+
+pBoolean :: ReadP.ReadP JSON
+pBoolean = pTrue ReadP.+++ pFalse
+
+pTrue :: ReadP.ReadP JSON
+pTrue = do
+  pSymbol "true"
+  pure (Boolean True)
+
+pFalse :: ReadP.ReadP JSON
+pFalse = do
+  pSymbol "false"
+  pure (Boolean False)
+
+pNumber :: ReadP.ReadP JSON
+pNumber = do
+  integer <- pInteger
+  (fraction, precision) <- ReadP.option (0, 0) pFraction
+  power <- ReadP.option 0 pPower
+  pWhitespaces
+  let mantissa = integer * 10 ^ precision + negateIf (integer <= 0) fraction
+  let magnitude = power - precision
+  pure (Number mantissa magnitude)
+
+pInteger :: ReadP.ReadP Integer
+pInteger = pZero ReadP.+++ pNonZero
+
+pZero :: ReadP.ReadP Integer
+pZero = do
+  ReadP.optional (ReadP.char '-')
+  Functor.void (ReadP.char '0')
+  pure 0
+
+pNonZero :: ReadP.ReadP Integer
+pNonZero = do
+  sign <- ReadP.option '+' (ReadP.char '-')
+  first <- ReadP.satisfy isNonZeroDigit
+  rest <- ReadP.munch isDecimalDigit
+  case fromDecimal (first : rest) of
+    Nothing -> ReadP.pfail
+    Just nonZero -> pure (negateIf (sign == '-') nonZero)
+
+pFraction :: ReadP.ReadP (Integer, Integer)
+pFraction = do
+  Functor.void (ReadP.char '.')
+  digits <- ReadP.munch1 isDecimalDigit
+  case fromDecimal digits of
+    Nothing -> ReadP.pfail
+    Just fraction -> pure (fraction, List.genericLength digits)
+
+pPower :: ReadP.ReadP Integer
+pPower = do
+  Functor.void (ReadP.char 'E' ReadP.+++ ReadP.char 'e')
+  sign <- ReadP.option '+' (ReadP.char '+' ReadP.+++ ReadP.char '-')
+  digits <- ReadP.munch1 isDecimalDigit
+  case fromDecimal digits of
+    Nothing -> ReadP.pfail
+    Just magnitude -> pure (negateIf (sign == '-') magnitude)
+
+pString :: ReadP.ReadP JSON
+pString = do
+  string <-
+    ReadP.between
+      (ReadP.char '"')
+      (ReadP.char '"')
+      (do characters <- ReadP.many pCharacter
+          pure (String characters))
+  pWhitespaces
+  pure string
+
+pCharacter :: ReadP.ReadP Char
+pCharacter = pLiteral ReadP.+++ pEscape
+
+pLiteral :: ReadP.ReadP Char
+pLiteral = ReadP.satisfy isLiteral
+
+pEscape :: ReadP.ReadP Char
+pEscape = do
+  Functor.void (ReadP.char '\\')
+  escape <- ReadP.get
+  case escape of
+    '"' -> pure '"'
+    '/' -> pure '/'
+    '\\' -> pure '\\'
+    'b' -> pure '\b'
+    'f' -> pure '\f'
+    'n' -> pure '\n'
+    'r' -> pure '\r'
+    't' -> pure '\t'
+    'u' -> do
+      digits <- ReadP.count 4 (ReadP.satisfy isHexadecimalDigit)
+      case fromHexadecimal digits of
+        Nothing -> ReadP.pfail
+        Just point -> pure (toEnum (fromIntegral point))
+    _ -> ReadP.pfail
+
+pArray :: ReadP.ReadP JSON
+pArray =
+  ReadP.between
+    (pSymbol "[")
+    (pSymbol "]")
+    (do values <- ReadP.sepBy pValue (pSymbol ",")
+        pure (Array values))
+
+pObject :: ReadP.ReadP JSON
+pObject =
+  ReadP.between
+    (pSymbol "{")
+    (pSymbol "}")
+    (do pairs <- ReadP.sepBy pPair (pSymbol ",")
+        pure (Object pairs))
+
+pPair :: ReadP.ReadP (String, JSON)
+pPair = do
+  String key <- pString
+  pSymbol ":"
+  value <- pValue
+  pure (key, value)
+
+-- * Rendering
+-- | Renders JSON as a string.
+--
+-- >>> showJSON Null
+-- "null"
+--
+-- >>> showJSON (Boolean True)
+-- "true"
+-- >>> showJSON (Boolean False)
+-- "false"
+--
+-- >>> showJSON (Number 0 0)
+-- "0e0"
+-- >>> showJSON (Number 12 34)
+-- "12e34"
+-- >>> showJSON (Number (-12) (-34))
+-- "-12e-34"
+--
+-- >>> showJSON (String "")
+-- "\"\""
+-- >>> showJSON (String "js")
+-- "\"js\""
+-- >>> showJSON (String "\"\\\b\f\n\r\t")
+-- "\"\\\"\\\\\\b\\f\\n\\r\\t\""
+-- >>> showJSON (String "\x1f")
+-- "\"\\u001f\""
+--
+-- >>> showJSON (Array [])
+-- "[]"
+-- >>> showJSON (Array [Null])
+-- "[null]"
+-- >>> showJSON (Array [Boolean True, Boolean False])
+-- "[true,false]"
+--
+-- >>> showJSON (Object [])
+-- "{}"
+-- >>> showJSON (Object [("", Null)])
+-- "{\"\":null}"
+-- >>> showJSON (Object [("t", Boolean True), ("f", Boolean False)])
+-- "{\"t\":true,\"f\":false}"
+showJSON :: JSON -> String
+showJSON json = sJSON json ""
+
+sJSON :: JSON -> ShowS
+sJSON json =
+  case json of
+    Null -> sNull
+    Boolean boolean -> sBoolean boolean
+    Number mantissa magnitude -> sNumber mantissa magnitude
+    String string -> sString string
+    Array array -> sArray array
+    Object object -> sObject object
+
+sNull :: ShowS
+sNull = showString "null"
+
+sBoolean :: Bool -> ShowS
+sBoolean boolean =
+  if boolean
+    then sTrue
+    else sFalse
+
+sTrue :: ShowS
+sTrue = showString "true"
+
+sFalse :: ShowS
+sFalse = showString "false"
+
+sNumber :: Integer -> Integer -> ShowS
+sNumber mantissa magnitude = shows mantissa . showChar 'e' . shows magnitude
+
+sString :: String -> ShowS
+sString string =
+  sSeparatedBetween (showChar '"') (showChar '"') id sCharacter string
+
+sCharacter :: Char -> ShowS
+sCharacter character =
+  case character of
+    '"' -> showString "\\\""
+    '\\' -> showString "\\\\"
+    '\b' -> showString "\\b"
+    '\f' -> showString "\\f"
+    '\n' -> showString "\\n"
+    '\r' -> showString "\\r"
+    '\t' -> showString "\\t"
+    _ ->
+      if isControl character
+        then showString "\\u" .
+             showString
+               (padLeft
+                  4
+                  '0'
+                  (toHexadecimal (fromIntegral (fromEnum character))))
+        else showChar character
+
+sArray :: [JSON] -> ShowS
+sArray array =
+  sSeparatedBetween (showChar '[') (showChar ']') (showChar ',') sJSON array
+
+sObject :: [(String, JSON)] -> ShowS
+sObject object =
+  sSeparatedBetween (showChar '{') (showChar '}') (showChar ',') sPair object
+
+sPair :: (String, JSON) -> ShowS
+sPair (key, value) = sString key . showChar ':' . sJSON value
+
+-- * Helpers
+fromBase :: Integer -> (Char -> Maybe Integer) -> String -> Maybe Integer
+fromBase b f s =
+  Monad.foldM
+    (\n c -> do
+       d <- f c
+       pure (b * n + d))
+    0
+    s
+
+fromDecimal :: String -> Maybe Integer
+fromDecimal s = fromBase 10 fromDecimalDigit s
+
+fromDecimalDigit :: Char -> Maybe Integer
+fromDecimalDigit c =
+  case c of
+    '0' -> Just 0
+    '1' -> Just 1
+    '2' -> Just 2
+    '3' -> Just 3
+    '4' -> Just 4
+    '5' -> Just 5
+    '6' -> Just 6
+    '7' -> Just 7
+    '8' -> Just 8
+    '9' -> Just 9
+    _ -> Nothing
+
+fromHexadecimal :: String -> Maybe Integer
+fromHexadecimal s = fromBase 16 fromHexadecimalDigit s
+
+fromHexadecimalDigit :: Char -> Maybe Integer
+fromHexadecimalDigit c =
+  case c of
+    'A' -> Just 10
+    'B' -> Just 11
+    'C' -> Just 12
+    'D' -> Just 13
+    'E' -> Just 14
+    'F' -> Just 15
+    'a' -> Just 10
+    'b' -> Just 11
+    'c' -> Just 12
+    'd' -> Just 13
+    'e' -> Just 14
+    'f' -> Just 15
+    _ -> fromDecimalDigit c
+
+isControl :: Char -> Bool
+isControl c = '\x00' <= c && c <= '\x1f'
+
+isDecimalDigit :: Char -> Bool
+isDecimalDigit c = '0' <= c && c <= '9'
+
+isHexadecimalDigit :: Char -> Bool
+isHexadecimalDigit c =
+  isDecimalDigit c || 'A' <= c && c <= 'F' || 'a' <= c && c <= 'f'
+
+isLiteral :: Char -> Bool
+isLiteral c = not (c == '"' || c == '\\' || isControl c)
+
+isNonZeroDigit :: Char -> Bool
+isNonZeroDigit c = '1' <= c && c <= '9'
+
+isWhitespace :: Char -> Bool
+isWhitespace c = c == '\t' || c == '\n' || c == '\r' || c == ' '
+
+negateIf :: Bool -> Integer -> Integer
+negateIf p n =
+  if p
+    then negate n
+    else n
+
+pSymbol :: String -> ReadP.ReadP ()
+pSymbol s = do
+  Functor.void (ReadP.string s)
+  pWhitespaces
+
+pWhitespaces :: ReadP.ReadP ()
+pWhitespaces = Functor.void (ReadP.munch isWhitespace)
+
+padLeft :: Integer -> a -> [a] -> [a]
+padLeft n x ys = reverse (padRight n x (reverse ys))
+
+padRight :: Integer -> a -> [a] -> [a]
+padRight n x ys =
+  if n <= 0
+    then ys
+    else case ys of
+           [] -> x : padRight (n - 1) x ys
+           y:zs -> y : padRight (n - 1) x zs
+
+runParser :: ReadP.ReadP a -> String -> Maybe a
+runParser p s =
+  Maybe.listToMaybe
+    (Maybe.mapMaybe
+       (\(x, t) ->
+          if null t
+            then Just x
+            else Nothing)
+       (ReadP.readP_to_S p s))
+
+sBetween :: ShowS -> ShowS -> (anything -> ShowS) -> anything -> ShowS
+sBetween left right render it = left . render it . right
+
+sSeparated :: ShowS -> (element -> ShowS) -> [element] -> ShowS
+sSeparated separator render elements =
+  case elements of
+    [] -> id
+    [element] -> render element
+    element:rest ->
+      render element . separator . sSeparated separator render rest
+
+sSeparatedBetween ::
+     ShowS -> ShowS -> ShowS -> (element -> ShowS) -> [element] -> ShowS
+sSeparatedBetween left right separator render elements =
+  sBetween left right (sSeparated separator render) elements
+
+toBase :: Integer -> (Integer -> Maybe Char) -> Integer -> String
+toBase b f n =
+  if n == 0
+    then [Maybe.fromJust (f n)]
+    else reverse (toBase' b f n)
+
+toBase' :: Integer -> (Integer -> Maybe Char) -> Integer -> String
+toBase' b f n =
+  case quotRem n b of
+    (0, 0) -> ""
+    (q, r) -> Maybe.fromJust (f r) : toBase' b f q
+
+toHexadecimal :: Integer -> String
+toHexadecimal n = toBase 16 toHexadecimalDigit n
+
+toHexadecimalDigit :: Integer -> Maybe Char
+toHexadecimalDigit n =
+  case n of
+    0 -> Just '0'
+    1 -> Just '1'
+    2 -> Just '2'
+    3 -> Just '3'
+    4 -> Just '4'
+    5 -> Just '5'
+    6 -> Just '6'
+    7 -> Just '7'
+    8 -> Just '8'
+    9 -> Just '9'
+    10 -> Just 'a'
+    11 -> Just 'b'
+    12 -> Just 'c'
+    13 -> Just 'd'
+    14 -> Just 'e'
+    15 -> Just 'f'
+    _ -> Nothing
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,4 @@
+import qualified Test.DocTest as Doctest
+
+main :: IO ()
+main = Doctest.doctest ["library"]
