diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright © 2017 Albert Krewinkel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+hslua-aeson
+===========
+
+Glue to hslua for aeson values.
+
+This provides a `StackValue` instance for aeson's `Value` type. The following
+conventions are used:
+
+- `Null` values are encoded as the special global `_NULL`. Using `Nil` would
+  cause problems with null-containing arrays.
+
+- Objects are converted to tables in a straight-forward way.
+
+- Arrays are converted to lua tables. Array-length is included as the value at
+  index 0. This makes it possible to distinguish between empty arrays and empty
+  objects.
+
+
+License
+-------
+
+This project is licensed under the liberal MIT license, the same license under
+which hslua and lua itself are published. See the [LICENSE](./LICENSE) file for
+details.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/hslua-aeson.cabal b/hslua-aeson.cabal
new file mode 100644
--- /dev/null
+++ b/hslua-aeson.cabal
@@ -0,0 +1,53 @@
+name:                hslua-aeson
+version:             0.1.0.0
+synopsis:            Glue between aeson and hslua
+description:         Please see README.md
+homepage:            https://github.com/tarleb/hslua-aeson#readme
+license:             MIT
+license-file:        LICENSE
+author:              Albert Krewinkel
+maintainer:          tarleb+hslua@zeitkraut.de
+copyright:           © 2017 Albert Krewinkel
+category:            Scripting
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Scripting.Lua.Aeson
+  build-depends:       base                 >= 4.7     && < 5
+                     , aeson                >= 0.11    && < 1.2
+                     , hashable             >= 1.2     && < 1.3
+                     , hslua                >= 0.4     && < 0.5
+                     , scientific           >= 0.3     && < 0.4
+                     , text                 >= 1.1.1.0 && < 1.3
+                     , unordered-containers >= 0.2     && < 0.3
+                     , vector               >= 0.7
+  default-language:    Haskell2010
+  ghc-options:         -Wall -fno-warn-unused-do-bind
+
+test-suite hslua-aeson-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             AesonSpec.hs
+  build-depends:       base
+                     , hspec            >= 2.2
+                     , HUnit
+                     , QuickCheck
+                     , quickcheck-instances
+                     , aeson
+                     , hashable
+                     , hslua
+                     , hslua-aeson
+                     , ieee754
+                     , scientific
+                     , text
+                     , unordered-containers
+                     , vector
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/tarleb/hslua-aeson
diff --git a/src/Scripting/Lua/Aeson.hs b/src/Scripting/Lua/Aeson.hs
new file mode 100644
--- /dev/null
+++ b/src/Scripting/Lua/Aeson.hs
@@ -0,0 +1,169 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-|
+Module      :  Scripting.Lua.Aeson
+Copyright   :  © 2017 Albert Krewinkel
+License     :  MIT
+
+Maintainer  :  Albert Krewinkel <tarleb@zeitkraut.de>
+Stability   :  experimental
+Portability :  portable
+
+Glue to hslua for aeson values.
+
+This provides a @StackValue@ instance for aeson's @Value@ type. The following
+conventions are used:
+
+- @Null@ values are encoded as the special global @_NULL@. Using @Nil@ would
+  cause problems with null-containing arrays.
+
+- Objects are converted to tables in a straight-forward way.
+
+- Arrays are converted to lua tables. Array-length is included as the value at
+  index 0. This makes it possible to distinguish between empty arrays and empty
+  objects.
+-}
+module Scripting.Lua.Aeson
+  ( module Scripting.Lua
+  , newstate
+  ) where
+
+import Data.HashMap.Lazy (HashMap)
+import Data.Hashable (Hashable)
+import Data.Scientific (Scientific, toRealFloat, fromFloatDigits)
+import Data.Text (Text)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Data.Vector (Vector, fromList, toList)
+import Scripting.Lua (LuaState, StackValue)
+
+import qualified Data.Aeson as Aeson
+import qualified Data.HashMap.Lazy as HashMap
+import qualified Data.Vector as Vector
+import qualified Scripting.Lua as Lua
+
+instance StackValue Scientific where
+  push lua n = Lua.pushnumber lua (toRealFloat n)
+  peek lua n = fmap fromFloatDigits <$>
+               (Lua.peek lua n :: IO (Maybe Lua.LuaNumber))
+  valuetype _ = Lua.TNUMBER
+
+instance StackValue Text where
+  push lua t = Lua.push lua (encodeUtf8 t)
+  peek lua i = fmap decodeUtf8 <$> Lua.peek lua i
+  valuetype _ = Lua.TSTRING
+
+instance (StackValue a) => StackValue (Vector a) where
+  push lua v = pushvector lua v
+  peek lua i = tovector lua i
+  valuetype _ = Lua.TTABLE
+
+instance (Eq a, Hashable a, StackValue a, StackValue b)
+      => StackValue (HashMap a b) where
+  push lua h = pushTextHashMap lua h
+  peek lua i = fmap HashMap.fromList <$> getPairs lua i
+  valuetype _ = Lua.TTABLE
+
+-- | Hslua StackValue instance for the Aeson Value data type.
+instance StackValue Aeson.Value where
+  push lua = \case
+    Aeson.Object o -> Lua.push lua o
+    Aeson.Number n -> Lua.push lua n
+    Aeson.String s -> Lua.push lua s
+    Aeson.Array a -> Lua.push lua a
+    Aeson.Bool b -> Lua.push lua b
+    Aeson.Null -> Lua.getglobal lua "_NULL"
+  peek lua i = do
+    ltype <- Lua.ltype lua i
+    case ltype of
+      Lua.TBOOLEAN -> fmap Aeson.Bool  <$> Lua.peek lua i
+      Lua.TNUMBER -> fmap Aeson.Number <$> Lua.peek lua i
+      Lua.TSTRING -> fmap Aeson.String <$> Lua.peek lua i
+      Lua.TTABLE -> do
+        Lua.rawgeti lua i 0
+        len <- Lua.peek lua (-1)
+        Lua.pop lua 1
+        case (len :: Maybe Int) of
+          Just _  -> fmap Aeson.Array <$> Lua.peek lua i
+          Nothing -> do
+            objlen <- Lua.objlen lua i
+            if objlen > 0
+              then fmap Aeson.Array <$> Lua.peek lua i
+              else do
+                isNull <- isLuaNull lua i
+                if isNull
+                  then return $ Just Aeson.Null
+                  else fmap Aeson.Object <$> Lua.peek lua i
+      Lua.TNIL -> return $ Just Aeson.Null
+      _        -> error $ "Unexpected type: " ++ (show ltype)
+  valuetype = \case
+    Aeson.Object _ -> Lua.TTABLE
+    Aeson.Number _ -> Lua.TNUMBER
+    Aeson.String _ -> Lua.TSTRING
+    Aeson.Array _ -> Lua.TTABLE
+    Aeson.Bool _ -> Lua.TBOOLEAN
+    Aeson.Null -> Lua.TTABLE
+
+-- | Create a new lua state suitable for use with aeson values. This behaves
+-- like @newstate@ in hslua, but initializes the @_NULL@ global. That variable
+-- is used to encode null values.
+newstate :: IO LuaState
+newstate = do
+  lua <- Lua.newstate
+  Lua.createtable lua 0 0
+  Lua.setglobal lua "_NULL"
+  return lua
+
+-- | Check if the value under the given index is lua-equal to @_NULL@.
+isLuaNull :: LuaState -> Int -> IO Bool
+isLuaNull lua i = do
+  let i' = if i < 0 then i - 1 else i
+  Lua.getglobal lua "_NULL"
+  res <- Lua.equal lua i' (-1)
+  Lua.pop lua 1
+  return res
+
+-- | Push a vector unto the stack.
+pushvector :: StackValue a => LuaState -> Vector a -> IO ()
+pushvector lua v = do
+  Lua.pushlist lua . toList $ v
+  Lua.push lua (Vector.length v)
+  Lua.rawseti lua (-2) 0
+
+-- | Try reading the value under the given index as a vector.
+tovector :: StackValue a => LuaState -> Int -> IO (Maybe (Vector a))
+tovector = fmap (fmap (fmap fromList)) . Lua.tolist
+
+-- | Try reading the value under the given index as a list of key-value pairs.
+getPairs :: (StackValue a, StackValue b)
+         => LuaState -> Int -> IO (Maybe [(a, b)])
+getPairs lua t = do
+  Lua.pushnil lua
+  pairs <- sequence <$> remainingPairs
+  return pairs
+ where
+  t' = if t < 0 then t - 1 else t
+  remainingPairs = do
+    res <- nextPair
+    case res of
+      Nothing -> return []
+      Just a  -> (a:) <$> remainingPairs
+  nextPair = do
+    hasNext <- Lua.next lua t'
+    if hasNext
+      then do
+        val <- Lua.peek lua (-1)
+        key <- Lua.peek lua (-2)
+        Lua.pop lua 1 -- removes the value, keeps the key
+        return $ Just <$> ((,) <$> key <*> val)
+      else do
+        return Nothing
+
+-- | Push a hashmap unto the stack.
+pushTextHashMap :: (StackValue a, StackValue b) => LuaState -> HashMap a b -> IO ()
+pushTextHashMap lua hm = do
+    let xs = HashMap.toList hm
+    Lua.createtable lua (length xs + 1) 0
+    let addValue (k, v) = Lua.push lua k *> Lua.push lua v *>
+                          Lua.rawset lua (-3)
+    mapM_ addValue xs
diff --git a/test/AesonSpec.hs b/test/AesonSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/AesonSpec.hs
@@ -0,0 +1,117 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Copyright   :  © 2017 Albert Krewinkel
+License     :  MIT
+
+Tests for Aeson–Lua glue.
+-}
+import Control.Monad (forM_, when)
+import Data.AEq ((~==))
+import Data.HashMap.Lazy (HashMap)
+import Data.Scientific (Scientific, toRealFloat, fromFloatDigits)
+import Data.Text (Text)
+import Data.Vector (Vector)
+import Scripting.Lua.Aeson (StackValue, newstate)
+import Test.Hspec
+import Test.HUnit
+import Test.QuickCheck
+import Test.QuickCheck.Instances ()
+
+import qualified Data.Aeson as Aeson
+import qualified Scripting.Lua as Lua
+
+-- | Run this spec.
+main :: IO ()
+main = hspec spec
+
+-- | Specifications for Attributes parsing functions.
+spec :: Spec
+spec = do
+  describe "Value component" $ do
+    describe "Scientific" $ do
+      it "can be converted to a lua number" $ property $
+        \x -> assert =<< luaTest "type(x) == 'number'" [("x", x::Scientific)]
+      it "can be round-tripped through the stack with numbers of double precision" $
+        property $ \x -> assertRoundtripEqual (luaNumberToScientific x)
+      it "can be round-tripped through the stack and stays approximately equal" $
+        property $ \x -> assertRoundtripApprox (x :: Scientific)
+    describe "Text" $ do
+      it "can be converted to a lua string" $ property $
+        \x -> assert =<< luaTest "type(x) == 'string'" [("x", x::Text)]
+      it "can be round-tripped through the stack" $ property $
+        \x -> assertRoundtripEqual (x::Text)
+    describe "Vector" $ do
+      it "is converted to a lua table" $ property $
+        \x -> assert =<< luaTest "type(x) == 'table'" [("x", x::Vector Bool)]
+      it "can contain Bools and be round-tripped through the stack" $ property $
+        \x -> assertRoundtripEqual (x::Vector Bool)
+      it "can contain Texts and be round-tripped through the stack" $ property $
+        \x -> assertRoundtripEqual (x::Vector Text)
+      it "can contain Vector of Bools and be round-tripped through the stack" $ property $
+        \x -> assertRoundtripEqual (x::(Vector (Vector Bool)))
+    describe "HashMap" $ do
+      it "is converted to a lua table" $ property $
+        \x -> assert =<< luaTest "type(x) == 'table'" [("x", x::HashMap Text Bool)]
+      it "can be round-tripped through the stack with Text keys and Bool values" $
+        property $ \x -> assertRoundtripEqual (x::HashMap Text Bool)
+      it "can be round-tripped through the stack with Text keys and Vector Bool values" $
+        property $ \x -> assertRoundtripEqual (x::HashMap Text (Vector Bool))
+    describe "Value" $ do
+      it "can be round-tripped through the stack" $ property $
+        \x -> assertRoundtripEqual (x::Aeson.Value)
+
+assertRoundtripApprox :: Scientific -> IO ()
+assertRoundtripApprox x = do
+  y <- roundtrip x
+  let xdouble = toRealFloat x :: Double
+  let ydouble = toRealFloat y :: Double
+  assert (xdouble ~== ydouble)
+
+assertRoundtripEqual :: (Show a, Eq a, StackValue a) => a -> IO ()
+assertRoundtripEqual x = do
+  y <- roundtrip x
+  assert (x == y)
+
+roundtrip :: (StackValue a) => a -> IO a
+roundtrip x = do
+  lua <- newstate
+  Lua.push lua x
+  size <- Lua.gettop lua
+  when (size /= 1) $
+    error ("not the right amount of elements on the stack: " ++ show size)
+  res <- Lua.peek lua (-1)
+  retval <- case res of
+        Nothing -> error "could not read from stack"
+        Just y  -> return y
+  Lua.close lua
+  return retval
+
+luaTest :: StackValue a => String -> [(String, a)] -> IO Bool
+luaTest luaTestCode xs = do
+  lua <- Lua.newstate
+  forM_ xs $ \(var, value) ->
+    Lua.push lua value *> Lua.setglobal lua var
+  let luaScript = "function run() return (" ++ luaTestCode ++ ") end"
+  Lua.openlibs lua
+  _ <- Lua.loadstring lua luaScript "test script"
+  Lua.call lua 0 0
+  retval <- Lua.callfunc lua "run"
+  Lua.close lua
+  return retval
+
+luaNumberToScientific :: Lua.LuaNumber -> Scientific
+luaNumberToScientific = fromFloatDigits
+
+instance Arbitrary Aeson.Value where
+  arbitrary = arbitraryValue 7
+
+arbitraryValue :: Int -> Gen Aeson.Value
+arbitraryValue size = frequency $
+    [ (1, return Aeson.Null)
+    , (4, Aeson.Bool <$> arbitrary)
+    , (4, Aeson.Number . luaNumberToScientific <$> arbitrary)
+    , (4, Aeson.String <$> arbitrary)
+    , (2, resize (size - 1) $ Aeson.Array <$> arbitrary)
+    , (2, resize (size - 1) $ Aeson.Object <$> arbitrary)
+    ]
